Signed OTA Updating

Introduction

Deploying embedded devices with (automatic) OTA functionality introduces new risks to local networks and the whole internet. If an attacker takes over the update server or runs a MITM attack, he might be able to turn the devices into zombies.

To prevent this, you can either provide a secure connection between device and update server (e. g. VPN or TLS) or add a cryptographic signature to all OTA files. Pull Request #893 provides hooks to the OTA functionality to allow checking of such signatures.

A proven method for this is, for example, ECDSA in conjunction with SHA-256. For both steps libraries are available (micro-ecc and Arduino Cryptosuite).

To use it, you can subclass RbootOutputStream like this:

#define PREFIX_MAGIC    0x54, 0x49, 0x55, 0x53
#define PREFIX_TYPE     0x00, 0x01
#define PREFIX_SIZE     sizeof(_my_prefix)
#define SIGNATURE_SIZE  64

const u8 _my_prefix[6] = { PREFIX_MAGIC, PREFIX_TYPE };

struct MyHdr {
    u8  prefix[PREFIX_SIZE];
    u8  signature[SIGNATURE_SIZE];
};

//-----------------------------------------------------------------------------
class MyStream : public RbootOutputStream {
public:
   MyStream(uint32_t startAddress, size_t maxLength = 0): RbootOutputStream(startAddress, maxLength)
   {
      // do some initialization if needed.
   }

   size_t write(const uint8_t* data, size_t size) override;
   bool close() override;
   virtual ~MyStream()
   {
     delete sha256;
   }

protected:
    bool init() override;

private:
    Sha256 *sha256 = nullptr;
    u8      hdr_len;
    MyHdr   hdr;
};

//-----------------------------------------------------------------------------
bool MyStream::init() {
    RbootOutputStream::init();
    delete sha256;
    sha256  = new Sha256;
    hdr_len = 0;
}

size_t MyStream::write(const uint8_t* data, size_t size) {
    //  store header
    u8 missing = sizeof(hdr) - hdr_len;
    if (missing) {
        if (size < missing) missing = size;
        memcpy( &hdr, data, missing );
        size    -= missing;
        data    += missing;
        hdr_len += missing;

        //  check prefix
        if ( hdr_len >= PREFIX_SIZE ) {
            if ( memcmp(hdr.prefix, _my_prefix, PREFIX_SIZE) ) {
                debugf("invalid prefix");
                return 0;
            }
        }
    }

    //  update hash
    sha256->update(data, size);

    //  save data
    return RbootOutputStream::write(data, size);
}

bool MyStream::close() {
    if (!RbootOutputStream::close()) {
      return false;
    }

    u8 hash[SHA256_BLOCK_SIZE];
    sha256->final( hash );

    bool sig_ok = /* add signature check here */;
    if (!sig_ok) {
        debugf("wrong signature");
        // TODO: if needed delete the block at the startAddress
        return 0;
    }
    return 1;
}

And then in your application you can use your MyStream with the following setup:

RbootHttpUpdater* otaUpdater = new RbootHttpUpdater();

MyStream* stream = new MyStream(1234); // Replace 1234 with the right start address

otaUpdater->addItem(ROM_0_URL, new MyStream()); // << the second parameter specifies that your stream will be used to store the data.

// and/or set a callback (called on failure or success without switching requested)
otaUpdater->setCallback(OtaUpdate_CallBack);