Howto X.509 on nRF91xx and LwM2M (for EN 18031 / RED-DA), the most basic way

[This writeup is a work in progress - it was meant to be a quickie, but to write it all down properly turned out to take a lot of time.]

Hey all,

I've been working on a family of commercially available cellular IoT devices using the LwM2M protocol utilizing nRF9160 and nRF9151 SoCs. In the European Union, those sort of things are mandated to have proper security since halfway 2025 already. As I write this, we're halfway 2026 now. But the road to piecing together this proper security for my product was kinda rocky, because documentation on the needed components was, and unfortunately still is, rather lacking.

So, here's my writeup of what I learned from the AT commands manual, Nordic modem firmware documentation, various issues on nRF DevZone (including this one!), random web pages, the OpenSSL docs and EU's rules & regulations docs. To save you the pain of having to figure it all out.

Preface

To sell certain types of products in the European Economic Area (EU + Norway & Iceland), they are required to meet or exceed certain standards. When a product is claimed to meet all the applicable standards for what type of product it is, it is to be designated Conformité Européenne, otherwise known as the CE mark. The General Product Safety Regulation (GPSR) stipulates a product must comply with all applicable standards to legally carry that CE mark.

CE in itself is not a certification. It is a self-proclaimed statement the product bearing it has passed all the applicable certification procedures, for it to be sold for use within the European Economic Area. When you're doing cellular IoT stuff, CE will include the Radio Equipment Directive. RED is all the run-of-the-mill radio testing stuff including electrical safety, EMF, EMC and your product not unintentionally being a radio jamming-device. Nothing exciting, has been around for years.

But then, the Radio Equipment Directive got extended in 2025: Published in January 2025 and in full effect since August 2025: EN 18031. Otherwise know as RED Cybersecurity and RED Delegated Act (RED-DA): https://en.wikipedia.org/wiki/Radio_Equipment_Directive_(2014)#Harmonised_standard_EN_18031

EN 18031 / RED Cybersecurity / RED Delegated Act (RED-DA)

The EN 18031 directive explicitly targets internet-connected radio-enabled devices. Otherwise commonly referred to as IoT devices. Developers (yes, that's you) have to up their security game to be allowed sell stuff for use in the European Economic Area.

There's a good deal of information on the web about EN 18031 / RED-DA. Be aware a lot of this information available on the public internet really is an infomercial / advertorial! Much about EN 18031 on the web today is written by certification companies that want you to use their services.

What these certification companies obscure on their infomercial websites is that you can self-certify the cybersecurity part of your product. For the run-of-the-mill radio testing stuff you will still need the certification company with their special rooms & radio equipment, but the cybersecurity-part is something you can do in-house yourself.

Be aware that all RED-DA documentation for your device must be available at first request for up to 10 years after the last sale. You lying about it being Conformité Européenne when it's not is legally punishable and it will likely trigger a forced recall of all affected devices, which you have to pay for. If your device is involved in some sort of cybersecurity fuckup, there's all sorts of liability issues coming your way.

So, you can't be & should not be Mr. Nice guy to yourself when doing your cybersecurity self-assessment. And if you decide to use the services of a certification company (EN 18031 calls them "notified bodies"), they will not be either.

What does EN 18031 really mean?

What EN 18031 comes down to is basically this: No more sloppy security practices allowed.

Proper security practices look like this:

  • All sensitive data going over your wireless connection is mathematically guaranteed to be confidential (encrypted).
  • Any attempt to alter sensitive data in transit (including a firmware update) is detected & dealt with (discarded).
  • Whomever you're communicating with must be cryptographically verified to be whom/what is claimed (authenticated).
  • Whenever sensitive data is accessed (over the network or local interfaces) it can only be done so by the intended (authorized) entities.
  • All sensitive data is stored in a secure memory (either by it being physically really hard to get to or by some electronic/logical means)
  • One device getting compromised despite these measures should not spoil them all.

Proper security: X.509 approach

In practice, for you (a developer of a nRF91xx device using LwM2M), it comes down to the following:

The following is a Kconfig choice, where the other option is CONFIG_APP_LWM2M_SECURITY_MODE_PSK=y
CONFIG_APP_LWM2M_SECURITY_MODE_X509=y

# The following is an arbitrary number chosen as ID, which needs to match with ID used
# in modem credential store (explained later)
CONFIG_LWM2M_CLIENT_UTILS_SERVER_TLS_TAG=0

# Size of each key resource in each security object instance;
# needs be large enough to fit the PSK or certificate as stored in the modem credential store
CONFIG_LWM2M_SECURITY_KEY_SIZE=1024

# Enable DTLS for transport security
CONFIG_LWM2M_DTLS_SUPPORT=y
CONFIG_LWM2M_DTLS_CID=y

I highly recommend you pick X.509 over using a PSK. You can pass CE while using the PSK approach, but to do PSK's the proper way (which is needed to pass CE!) actually is way more cumbersome than the X.509 approach.

The main reason PSK does not do, is due to how it is done in the lwm2m_client sample: The PSK is set by the firmware and hence the key is in the flash image. If you compile your PSK into program code, all devices will have the same key.

This is very much not desirable: One device getting compromised, spoils them all. Also the key being in the flash image means it's transmitted in over the air updates. Is your transmission channel CoAP or HTTP? Your PSK is then transmitted in the clear at some point, during which it can be easily intercepted. Is your updated flash image stored in external flash memory before being applied? External flash is not secure. The flash image leaks by other means? All devices compromised.

Using a PSK in your flash image will not pass EN 18031 / RED-DA.

Provisioning during production

During device production, you need create some sort of individualization-step. nRF91xx devices already are individuals by means of all having a unique IMEI. But they also need unique keys.

You can pass CE with a PSK, if you program them all with a unique PSK. There's no technical limitation on provisioning them all with the same PSK, but doing so means one key-leaking oops spoils all your devices.

Provisioning all your devices with the same PSK will not pass EN 18031 / RED-DA.

As such, all your devices will need a unique key. And when you're at the point of all devices getting a unique PSK, you're actually easier off with X.509 than trying to steer clear of it.

Why? Because you need track & save every unique PSK, and properly store it such you know what device (IMEI) you programmed what PSK into. This is not an issue with X.509, using which you can create a chain of trust. Using this chain of trust, you can verify each device certificate using the higher-level certificate, which means you not need store the certificates you have programmed into your devices. All you need do is set up a proper chain-of-trust using the X.509 scheme. More on how to do this later (but reading up on it now already won't hurt you).

Modem credential store (1)

With nRF91xx devices, provisioning is most effectively done using the modem credential store. What this is and how it works is already touched upon by the very basic provisioning guide in the Nordic documentation coming with the lwm2m_client sample.

In short: The modem credential store is a piece of memory managed by the modem firmware, only accessible by the modem firmware. It can store X.509 certificates, private keys and pre-shared keys. It groups those by type, and certain types have restrictions. Most important for us: A private key can only be written to the credential store, it can never be read. This counts as a secure memory under the terms of EN 18031.

Programming public certificates and private keys into the modem credential store will pass EN 18031 / RED-DA.

Once PSK's, certificates, private keys are in the modem credential store, the modem firmware can directly use them when setting up connections for you.

Generating X.509 certificates

As the title of this writing tells you, I explain doing X.509 in the most basic way. If you are a big organization you'd be running some PKI (public key infrastructure) and signing authority yourself, or you'd be outsourcing it somewhere. But then you'd already have an expert available on this topic and not be reading this. So you're likely not that big. And you not have an expert at hand.

At this point, I'm assuming you know what X.509 is, and what a chain of trust is. Now we're going to make you one.

Root certificate

The following should work on any recently updated Linux box with OpenSSL installed:

# Generate a 256-bit eliptic-curve private key. Nobody but you should have it
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out myDevicesRootCA.key

# Generate a public X.509 signing certificate, using the above generated private key. Everybody can have this certificate.
# This command will be interactive; It will ask you for:
# Your 2-letter country code
# State or province name
# Organization (company) name
# Unit (department) name
# Common name (typically the URL this certificate can be downloaded from)
# Your email address
# If you enter a . (dot) to a question, the requested field will be left empty
openssl req -x509 -new -sha256 -days 3650 -key myDevicesRootCA.key -out myDevicesRootCA.crt -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"

There it is: myDevicesRootCA.crt

You are a certification authority now. The file myDevicesRootCA.crt can be used by anyone/anything to verify a signature by you was really you. That is, so long you guard myDevicesRootCA.key well. No one should have it but whom need sign device certificates.

Device certificate

# Generate a new private key for your first device
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out myDevice.key

# Generate a new certificate for your first device, signed with the root CA certificate you generated in the previous code block
openssl req -x509 -new -sha256 -days 5500 -key myDevice.key -CA myDevicesRootCA.crt -CAkey myDevicesRootCA.key -subj /CN=urn:imei:000000000000000/ -out myDevice.crt

There it is: myDevice.crt and a corresponding myDevice.key for your first device.

Device certificate validation

Whenever a device connects to a LwM2M endpoint, it presents the other end with its certificate (this is part of the TLS/DTLS protocols). The other end (either a LwM2M endpoint you host yourself, or a LwM2M endpoint service you rent) then needs means to validate the certificate.

Anyone can generate a device certificate. But only you can generate a device certificate signed with your root CA certificate. Using your myDevicesRootCA.crt, any myDevice.crt can be validated as a legit one; myDevice0000.crt all the way through to myDevice9999.crt with the same file.

This is the beauty of X.509: You not need keep a record of all the public certificates you generated and put in your devices (together with the matching private key). Every device will present its certificate on TLS/DTLS connection setup, and the only thing you need to validate the legitimacy of those certificates is your own one root CA certificate. All you need supply your LwM2M endpoint with is this one certificate.

This is in stark contrast to PSK: If you give every device a unique PSK (which you need to pass CE!) then you need record every PSK and IMEI it is used with. And these records then need be kept up to date with your LwM2M endpoint (which you either host yourself or rent externally). This is a painful and error-prone task, far greater than the daunting task of figuring out X.509 properly.

Endpoint certificate validation

Not only the LwM2M endpoint validates a device based on its certificate, the reverse should also be true: Your device should validate the certificate presented by the endpoint on TLS/DTLS initialization. Doing this counts as an authorization mechanism for EN 18031 / RED-DA.

Remember, one of the requirements was: Whenever sensitive data is accessed it can only be done so by the intended entities. The endpoint must be authenticated as the correct endpoint to be considered authorized accessing the device data.

Figuring out what certificate to use for validating the server certificate is a bit tricky, but once understood not hard. Again, OpenSSL (as installed on any recently updated Linux box) comes to the rescue:

# Fetch the certificate chain as presented by the other end on connection, store it in file cert_chain.crt
echo | openssl s_client -dtls -connect your.lwm2m.endpoint.eu:5684 -showcerts 2>/dev/null | awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/{print}' > cert_chain.crt

# Certificates in cert_chain.crt are stored in PEM (Privacy-Enhanced Mail) format, which is Base64 encoded binary data.
# To decode this chain of certificates, again OpenSSL is used to convert Base64 encoded binary to PKCS#7,
# which is then again piped into OpenSSL to convert it into human-readable data. This output is then grepped (searched) for lines containing "CA Issuers"
openssl crl2pkcs7 -nocrl -certfile cert_chain.crt | openssl pkcs7 -print_certs -text -noout | grep "CA Issuers"

What comes out of the last command is dependent on how the LwM2M endpoint maintainer (you, or someone you hired) has set up the TLS on the server in question. One can make chains of trust in all sorts of creative ways.

Usually the certificate chain supplied by the server consists of one or multiple certificates with a relatively short lifespan: Certificates expire and rotate relatively often. Usually the last certificate in the supplied chain refers to a much longer lived certificate. It is this long-lived certificate (10+ years validity) you want ship inside your device's modem credential store when it goes out in the field.

Whenever your device connects to the LwM2M endpoint, this server supplies it with its chain of trust, which can be validated using this long-lived certificate. Download this certificate from the given URL. When you download the file, it likely contains a lot of details and information. Cut this file down to -----BEGIN CERTIFICATE----------END CERTIFICATE-----. Only this header & footer & everything in between needs be in this file (unaltered). All the other data wastes precious space in the modem credential store.

Modem credential store (2)

You now have the long-lived certificate which can be used to validate the chain of trust supplied by your LwM2M endpoint server, the certificate of your device, the private key of your device (the last two in this list you generated yourself using instructions above).

These three pieces of information need find their way to the modem credential store. The easiest way to do this is by issuing AT commands.

The AT-command shell of Zephyr however has too small a buffer for most certificates. As such, it needs be done over a UART or from program code using nrf_modem_at_cmd(). The basic provisioning guide in the Nordic docs does it by flashing the AT sample code (which has a way larger buffer than the Zephyr shell) and a Python script.

However you do it is up to you. I did it by having a hidden function in my firmware which calls nrf_modem_at_cmd(). See the following pseudo'ish code:

void modem_store_creds(char **credentials_buffers) // An array of pointers to buffers
{
    static char response[128];
    int err = 0;

    // Escaping " with a \, escaping % by doing %%
    constexpr auto atdeleteitem = "AT%%CMNG=3,%d,%d";
    constexpr auto atwriteitem = "AT%%CMNG=0,%d,%d,\"%s\"";

    // It is assumed three buffers supplied as argument have been written with the required X.509 security credentials.
    // In the first buffer, a certificate suitable for validating the server certificate chain. In the second, a public certificate
    // to be used (send on connection) by the device, and lastly the private key belonging to the public key in the device certificate.
    // These three buffers are then used as content in three separate AT commands send to the modem
    for(uint8_t i = 0; i < 3; i++) {
        memset(response, NULL, sizeof(response));

        err = nrf_modem_at_cmd(response, sizeof(response), atdeleteitem, CONFIG_LWM2M_CLIENT_UTILS_SERVER_TLS_TAG, i);
        if(nrf_modem_at_err(err)) {
            LOG_ERR("Failed purging security item %i: %i (%i)", i, nrf_modem_at_err(err), err);
        }

        LOG_DBG("Purge security item response %i:\r\n%s", i, response);

        k_sleep(K_MSEC(250));

        memset(response, NULL, sizeof(response));

        err = nrf_modem_at_cmd(response, sizeof(response), atwriteitem, CONFIG_LWM2M_CLIENT_UTILS_SERVER_TLS_TAG, i, credentials_buffers[i]);
        if(nrf_modem_at_err(err)) {
            LOG_ERR("Failed storing security item %i: %i (%i)", i, nrf_modem_at_err(err), err);
        }

        LOG_DBG("Store security item response %i:\r\n%s", i, response);

        k_sleep(K_SECONDS(1));
    }
}

However you write these buffers before feeding them to the modem is up to you. We did it using the debug probe (before the debug port gets permanently shut off).

EN 18031 (self-)certification

Got your device ready, with proper security in place? Now you need do your self-certification.

See here: https://github.com/zealience/IoT-Cybersecurity-Compliance/tree/main/EN18031-Templates

Also, you need write up a technical file for all the details not covered by the templates kindly provided to you by Zealience.

[Writing will continue here; there's a lot more to write down.]

Parents
  • The same key for encryption & decryption, in both directions.

    As long as you use TLS/DTLS 1.2/1.3, both, PSK or ECDSA keys, are only used for authentication and the keys for encryption/decryption are "negotiated" during the handshake.

    The modem keeps those "long term" authentication keys (PSK or ECDSA) in a separate storage, so you could provide them during "production setup", it's not required to supply them in the application firmware.

    When done correctly, the encryption is asymmetrical:

    That doesn't apply to TLS/DTLS 1.2/1.3.

    Check for example TLS_ECDHE_ECDSA_WITH_AES_256_CCM, TLS_PSK_WITH_AES_256_CCM, or TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384

    They all use AES 256, negotiated symmetric keys for encryption/decryption.

    The asymmetric keys of ECDSA are only used for authentication (e.g. signing the ECDHE).

    If a "authentication" key leaks, then the consequences depends more on the key-exchange.

    If PFS is used (e.g. ECDHE), that cause someone will be able to establish a session, but not enable to decrypt data of an other session.

    All in all:

    TLS (and DTLS) is a mature encryption framework, which handles quite a lot of the common challenges in a pretty good way. No need to overdo the things more ;-).

  • The modem keeps those "long term" authentication keys (PSK or ECDSA) in a separate storage, so you could provide them during "production setup", it's not required to supply them in the application firmware.

    Yes that's what I'm about to get into with X.509 as I now continue writing, because without that approach one not passes for CE anymore. However the lwm2m example code not do this, which will cause at least some developers to no use the correct approach.

    As long as you use TLS/DTLS 1.2/1.3, both, PSK or ECDSA keys, are only used for authentication and the keys for encryption/decryption are "negotiated" during the handshake.

    Yeah that's with full TLS. For as far I could find with DTLS (for CoAP, UDP) as used by LwM2M, Diffie-Hellman key exchange is not executed, for complexity/memory reasons. Are you sure in PSK mode the connection still gets a session-key?

  • Are you sure in PSK mode the connection still gets a session-key?

    Yes, I'm sure. (I'm one of the developers of Eclipse/Californium CoAP+DTLS and Eclipse/tinyDTLS.)

  • However the lwm2m example code not do this,

    When LwM2M was discussed and specified at the OMA (I was active there in 2015/16), several different bootstrap processes have been defined. Including a "factory bootstrap", but also a "bootstrap-server" based approach. If that makes sense in your case is left to you. The demo client itself demonstrates, how to use a "bootstrap-server", but you may adapt it to the "factory bootstrap" approach, if that fits better for you.

    If newer regulations refuse some options, then that's more something common.

    Same happens to the selected mandatory cipher suites. At that time in 2015, the decision was to support (among others) TLS_PSK_WITH_AES_128_CCM_8, which in our days would be rather TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256 (ECDHE + 16 bytes MAC). But that's the flow of time ;-).

    By the way, LwM2M TS 1.0 already says in 7.1.3 Ciphersuites:

    "LwM2M Clients and LwM2M Servers MAY support additional ciphersuites that conform to state-of-the-art security requirements."

    In general, I would recommend to discus such topics, as compliance to different regulations, at the OMA.

Reply
  • However the lwm2m example code not do this,

    When LwM2M was discussed and specified at the OMA (I was active there in 2015/16), several different bootstrap processes have been defined. Including a "factory bootstrap", but also a "bootstrap-server" based approach. If that makes sense in your case is left to you. The demo client itself demonstrates, how to use a "bootstrap-server", but you may adapt it to the "factory bootstrap" approach, if that fits better for you.

    If newer regulations refuse some options, then that's more something common.

    Same happens to the selected mandatory cipher suites. At that time in 2015, the decision was to support (among others) TLS_PSK_WITH_AES_128_CCM_8, which in our days would be rather TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256 (ECDHE + 16 bytes MAC). But that's the flow of time ;-).

    By the way, LwM2M TS 1.0 already says in 7.1.3 Ciphersuites:

    "LwM2M Clients and LwM2M Servers MAY support additional ciphersuites that conform to state-of-the-art security requirements."

    In general, I would recommend to discus such topics, as compliance to different regulations, at the OMA.

Children
No Data
Related