ESP-IDF Tutorial: A proposal for verifying data from embedded devices
I'll soon return to the basic tutorials on the ESP-IDF. This tutorial is a one-off on a topic I find interesting.
"How can I validate that data from an embedded device came from that device?"
This sounds like a job for public key cryptography. I'm no crypto engineer by any means, but I can certainly use libraries. The ESP IDF ships with mbedtls, which is a surprisingly comprehensive suite of crypto. The library is owned by some people called ARM mbed, so I am guessing you can run it on any ARM.
Here's the scheme:
- Create a public/private key pair (RSA in this example) on the embedded device and store it in memory. In our case in the SPIFFS file system.
- Send the public key to the server
- Hash and sign every data packet you will send the server with the private key.
- Embed the signature as a header in the HTTP request
- Validate the signature on the server when receiving data
Notes:
- If you are using IDF 4.0 you will need to add "mbedtls" to you CMakeLists.txt.
- You may well need a slight increase in the default stack size of 3k or so
Includes
#include "mbedtls/base64.h"
#include "mbedtls/config.h"
#include "mbedtls/platform.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/bignum.h"
#include "mbedtls/x509.h"
#include "mbedtls/rsa.h"
#include "mbedtls/md.h"
#define VIRC_CRYPTO_OK (0)
#define VIRC_CRYPTO_KEY_SIZE (512)
#define VIRC_CRYPTO_KEY_EXPONENT (65537)
#define VIRC_CRYPTO_PRIVKEY_FILENAME ("/storage/rsa_priv.txt")
#define VIRC_CRYPTO_PUBKEY_FILENAME ("/storage/rsa_pub.txt")
#define VIRC_CRYPTO_SIG_LENGTH (64)
#define VIRC_CRYPTO_HASH_LENGTH (128)
Generating an RSA public, private key pair and storing it in SPIFFS
static const char* TAG = "virc-crypto";
// static variables we use for generating and loading
static mbedtls_rsa_context rsa;
static mbedtls_md_context_t md;
static mbedtls_entropy_context entropy;
static mbedtls_ctr_drbg_context ctr_drbg;
static mbedtls_mpi N, P, Q, D, E, DP, DQ, QP;
static bool
keygen
(void)
{
ESP_LOGI(TAG, "RSA keygen");
uint8_t err;
// init ALL the things
mbedtls_ctr_drbg_init( &ctr_drbg );
mbedtls_rsa_init( &rsa, MBEDTLS_RSA_PKCS_V15, 0 );
mbedtls_mpi_init( &N );
mbedtls_mpi_init( &P );
mbedtls_mpi_init( &Q );
mbedtls_mpi_init( &D );
mbedtls_mpi_init( &E );
mbedtls_mpi_init( &DP );
mbedtls_mpi_init( &DQ );
mbedtls_mpi_init( &QP );
mbedtls_entropy_init( &entropy );
// We use an ID here that is the mac address of the device.
// It is the "personalization" data. You can use what you like
err = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
virc_core_id(), VIRC_CORE_ID_LEN);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_ctr_drbg_seed (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_ctr_drbg_seed");
err = mbedtls_rsa_gen_key(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg,
VIRC_CRYPTO_KEY_SIZE, VIRC_CRYPTO_KEY_EXPONENT);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_gen_key (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_gen_key");
err = mbedtls_rsa_export(&rsa, &N, &P, &Q, &D, &E);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_export (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_export");
err = mbedtls_rsa_export_crt( &rsa, &DP, &DQ, &QP );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_export_crt (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_export_crt");
// Key is now generated, we need to write the various bits to files
// First the public key
FILE *fpub = fopen(VIRC_CRYPTO_PUBKEY_FILENAME, "wb+");
if (fpub == NULL) {
ESP_LOGE(TAG, "failed fopen pub (wb+) (%s)", VIRC_CRYPTO_PUBKEY_FILENAME);
return false;
}
ESP_LOGD(TAG, "ok fopen pub (%s)", VIRC_CRYPTO_PUBKEY_FILENAME);
err = mbedtls_mpi_write_file( "N = ", &N, 16, fpub );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file pub N= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file pub N=");
err = mbedtls_mpi_write_file( "E = ", &E, 16, fpub );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file pub E= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file pub E=");
err = fclose( fpub );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed fclose pub (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok fclose pub");
// Now the private key
FILE *fpriv = fopen(VIRC_CRYPTO_PRIVKEY_FILENAME, "wb+");
if (fpub == NULL) {
ESP_LOGE(TAG, "failed fopen priv (wb+) (%s)", VIRC_CRYPTO_PRIVKEY_FILENAME);
return false;
}
ESP_LOGD(TAG, "ok fopen priv (%s)", VIRC_CRYPTO_PRIVKEY_FILENAME);
err = mbedtls_mpi_write_file( "N = " , &N , 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv N= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv N=");
err = mbedtls_mpi_write_file( "E = " , &E , 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv E= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv E=");
err = mbedtls_mpi_write_file("D = " , &D , 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv D= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv D=");
err = mbedtls_mpi_write_file("P = " , &P , 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv P= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv P=");
err = mbedtls_mpi_write_file("Q = " , &Q , 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv Q= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv Q=");
err = mbedtls_mpi_write_file("DP = ", &DP, 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv DP= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv DP=");
err = mbedtls_mpi_write_file("DQ = ", &DQ, 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv DQ= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv DQ=");
err = mbedtls_mpi_write_file("QP = ", &QP, 16, fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_write_file priv QP= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_write_file priv QP=");
err = fclose( fpriv );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed fclose priv (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok fclose priv");
// Free the heap objects
mbedtls_mpi_free(&N);
mbedtls_mpi_free(&P);
mbedtls_mpi_free(&Q);
mbedtls_mpi_free(&D);
mbedtls_mpi_free(&E);
mbedtls_mpi_free(&DP);
mbedtls_mpi_free(&DQ);
mbedtls_mpi_free(&QP);
mbedtls_rsa_free(&rsa);
mbedtls_ctr_drbg_free(&ctr_drbg);
mbedtls_entropy_free(&entropy);
ESP_LOGI(TAG, "ok RSA keygen");
return true;
}
Well, that wasn't exactly fun, was it. It did take me a while to work it out from examples in the mbedtls code base and I am sure you can successfully modify it.
Loading the private key for signing
Now that we have stored the key, we need to load it into memory from disk. It is assumed you only need to generate it one time, say on first load, or after reformatting the file system, but you will have to load it every time. We use the same variables above and note that we only load the private key. The public key is not of any use to the embedded device any more, except to send it to the server to use to validate our signatures.
The following function shows how to load the key we just generated into memory.
bool keyload() {
mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V15, 0);
mbedtls_mpi_init(&N);
mbedtls_mpi_init(&P);
mbedtls_mpi_init(&Q);
mbedtls_mpi_init(&D);
mbedtls_mpi_init(&E);
mbedtls_mpi_init(&DP);
mbedtls_mpi_init(&DQ);
mbedtls_mpi_init(&QP);
uint8_t err;
ESP_LOGI(TAG, "RSA read");
FILE* fpriv = fopen(VIRC_CRYPTO_PRIVKEY_FILENAME, "rb");
if (fpriv == NULL) {
ESP_LOGE(TAG, "failed fopen priv (%s)", VIRC_CRYPTO_PRIVKEY_FILENAME);
return false;
}
ESP_LOGD(TAG, "ok fopen (rb) (%s) priv", VIRC_CRYPTO_PRIVKEY_FILENAME);
err = mbedtls_mpi_read_file( &N , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv N= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv N=");
err = mbedtls_mpi_read_file( &E , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv E= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv E=");
err = mbedtls_mpi_read_file( &D , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv D= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv D=");
err = mbedtls_mpi_read_file( &P , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv P= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv P=");
err = mbedtls_mpi_read_file( &Q , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv Q= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv Q=");
err = mbedtls_mpi_read_file( &DP , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv DP= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv DP=");
err = mbedtls_mpi_read_file( &DQ , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv DQ= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv DQ=");
err = mbedtls_mpi_read_file(&QP , 16, fpriv);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_mpi_read_file priv QP= (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_mpi_read_file priv QP=");
err = mbedtls_rsa_import( &rsa, &N, &P, &Q, &D, &E );
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_import (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_import");
err = mbedtls_rsa_complete(&rsa);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_complete (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_complete");
err = mbedtls_rsa_check_privkey(&rsa);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_check_privkey (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_check_privkey");
mbedtls_mpi_free(&N);
mbedtls_mpi_free(&P);
mbedtls_mpi_free(&Q);
mbedtls_mpi_free(&D);
mbedtls_mpi_free(&E);
mbedtls_mpi_free(&DP);
mbedtls_mpi_free(&DQ);
mbedtls_mpi_free(&QP);
ESP_LOGI(TAG, "ok RSA read");
return true;
}
Signing data with our key
// For a given buffer buf, of size len, put the signature into the array sig.
// sig will be base64 encoded and suitable for use as a HTTP header
bool virc_crypto_sign
(uint8_t *buf, size_t len, uint8_t sig[128])
{
ESP_LOGI(TAG, "md5 sha256 rsa sign");
uint8_t err = -1;
mbedtls_md_init(&md);
err = mbedtls_md_setup(&md, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_md_setup (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_md_setup");
err = mbedtls_md_starts(&md);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_md_starts (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_md_starts");
err = mbedtls_md_update(&md, buf, len);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_md_update (len=%d)", len);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_md_update (len=%d)", len);
uint8_t hash[32];
err = mbedtls_md_finish(&md, hash);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_md_finish (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_md_finish");
uint8_t raw[VIRC_CRYPTO_SIG_LENGTH];
err = mbedtls_rsa_pkcs1_sign(&rsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, MBEDTLS_MD_SHA256, 20, hash, raw);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_rsa_pkcs1_sign (%d) ", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_rsa_pkcs1_sign");
mbedtls_md_free(&md);
size_t olen;
err = mbedtls_base64_encode(sig, VIRC_CRYPTO_HASH_LENGTH, &olen, raw, VIRC_CRYPTO_SIG_LENGTH);
if (err != VIRC_CRYPTO_OK) {
ESP_LOGE(TAG, "failed mbedtls_base64_encode (%d)", err);
return false;
}
ESP_LOGD(TAG, "ok mbedtls_base64_encode (len=%d)", olen);
ESP_LOGI(TAG, "ok md5 sha256 rsa sign");
return true;
}
And we are done. Now what?
- The same entity that generated and sent us the public key also sent us this data
- The data has not been modified en route