-
Notifications
You must be signed in to change notification settings - Fork 306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OTA: Verify firmware integrity before applying firmware update #172
Comments
+1 I was thinking about doing some sort of hash verification on my own... It would be great to be on the Homie itself. Sent from my iPhone
|
Definitely! Actually, I wanted to implement this, as as you said this is already implemented in the I'm also 👍 for the |
I also don't think MD5 collisions are a problem. The "md5:" prefix was just intended to keep a door open for potential future extensions to the signature scheme without having to touch the Homie convention. I was hoping that maybe some day a (simple) code signing scheme can be added to Homie. I may have read too many news about IoT bot nets recently... ;-) |
Yes, I guess we would better apply the KISS principle in this case, at least for the ESP8266. And anyway, as discussed, OTA is implementation dependant, so we would not have to update the convention to support a new signature scheme. |
I spent some time studying the esp8266/arduino
Regarding MD5 checks:
Regarding firmware name checks:
I guess bottom line is that the most vital checks (firmware size, firmware magic bytes) are in place already. I think we should add optional MD5 checking but we would have to live with the all-or-nothing (i.e. always-or-never) limitation until esp8266/Arduino gets an update, or we would need to reboot if a firmware update fails to make MD5 truly optional. I would NOT suggest checking the firmware name or Homie's magic bytes. What are your thoughts? |
I guess folks won't update their firmware "by hand" anyway, they will use some sort of script to automate the MQTT messages needed for the update. So actually requiring publishing an MD5 would not harm, as MD5 is available in almost every language. What do you think? I agree with letting the ability to "swap firmwares", so indeed checking the name might not be a good idea. And, as you say, we would need to use a state machine which might be too much hassle. |
I can now work on this one. The OTA sequence I envision is as follows:
Is the sequence OK? Can we rename |
I cannot fault the sequence; sounds good to me.
+1 to renaming to `$implementation/ota/firmware` -- makes more sense.
|
From my experience the binary-data breaks some(most?) websocket-clients. +1 for renaming to |
I would also prefer base64-encoded firmware blobs (see @jpmen's proposal #143) because I keep having issues with binary firmware messing/locking up my Luckily, ESP firmwares always have a Would this be acceptable? |
Oh, I forgot to mention that with the sequence I suggested there's no need to retain OTA messages anymore. None of them. I think retention was only required because, before Homie added status messages, the server was unable to find out whether Homie accepted an Retaining firmware blobs also puts unneeded burden on the MQTT server (having to retain 350KB or so for each homie node). |
The sequence sounds good to me, and yes, let's rename About the base64, as @mrpace2 said, there's a way to differentiate a binary firmware from a base64 firmware, so let's go this way. You can use cdecode.h from the core, with |
Thanks. Will do. |
I think we're now feature-complete with respect to the original objective of this issue. Thanks for discussing options and for accepting my contributions. Closing the issue. |
@mrpace2 thanks a lot! I made two commits after yours: 083ab27 and 0a060a4 Although the changes are not significant, I am not able to test right now. Can you test the latest git rev? One question, also: // In case of base64-coded firmware, we have given the wrong length to Update.begin()
// because the base64-encoded firmware may have contained non-base64 characters such
// as line feeds You mean line feeds at the end? I'm uncomfortable with that, the payload should be clean. |
Reopened because of @marvinroger's comment. The comment you asked about is a leftover from a previous version of the commit where I was still hoping that I can support line-wrapping in the base64-encoded payload. The comment doesn't make sense now. I will rephrase it tomorrow. Thanks for pointing that out. The code is right, though. The payload is clean. The actual number of bytes written to firmware might still be off by 1 from the length we set in I will test your commits tomorrow. |
@marvinroger: I tried 0a060a4 on my setup with my little test script. It seems to work. I called my script 3 times, so installing the same OTA update 3 times (I am forcing the update by purposely sending the wrong firmware version in the script). Log files (removed my IDs and cropped the firmware blob):
|
Got it! I was always wondering what were the |
Here is a quick'n'dirty Node.js example:
'use strict'
const BROKER = '127.0.0.1'
const BASE_TOPIC = 'homie/'
const DEVICE_ID = 'esp8266-test'
const PREFIX = `${BASE_TOPIC}${DEVICE_ID}`
const path = require('path')
const mqtt = require('mqtt')
const firmware = require('fs').readFileSync(path.join(__dirname, './assets/firmware.bin'))
const checksum = require('crypto').createHash('md5').update(firmware).digest('hex')
const client = mqtt.connect(`mqtt://${BROKER}`)
client.on('connect', () => {
client.subscribe(`${PREFIX}/#`)
})
let startTime
let step = 'INITIAL'
client.on('message', (topic, message) => {
topic = topic.substr(PREFIX.length + 1)
message = message.toString()
switch (step) {
case 'INITIAL':
if (topic === '$fw/version') {
startTime = Date.now()
client.publish(`${PREFIX}/$ota`, '2.0.0')
step = 'REQUESTED'
console.log('Requested OTA')
}
return
case 'REQUESTED':
if (topic === '$implementation/ota/status') {
const status = parseInt(message.split(' ')[0], 10)
if (status !== 202) {
console.log('OTA not accepted')
process.exit(1)
}
console.log('Accepted, sending checksum')
step = 'CHECKSUM'
client.publish(`${PREFIX}/$implementation/ota/checksum`, checksum)
}
return
case 'CHECKSUM':
if (topic === '$implementation/ota/status') {
const status = parseInt(message.split(' ')[0], 10)
if (status !== 202) {
console.log('Checksum not accepted')
process.exit(1)
}
console.log('Accepted, sending firmware')
step = 'SENDING'
client.publish(`${PREFIX}/$implementation/ota/firmware`, firmware)
}
return
case 'SENDING':
if (topic === '$implementation/ota/status') {
const splitted = message.split(' ')
const status = parseInt(splitted[0], 10)
if (status === 206) {
console.log(`OTA in progress... ${splitted[1]}`)
} else if (status === 200) {
console.log('OTA success')
console.log(`Done in ${Date.now() - startTime}ms`)
process.exit(0)
} else {
console.log('OTA error')
process.exit(1)
}
}
return
}
})
In my last test, it took 6400ms for a 338kb firmware, which sounds good. |
@marvinroger I think they are, yes. You two gentlemen have done marvellous stuff. I won't be able to play with this for the next three weeks, but will try when I'm back at base. |
Great! I think we're done here, then. @mrpace2 did the marvellous stuff, here. 😉 |
The few lines that I contributed are just a drop in the ocean. Thanks to @marvinroger for creating Homie. And for maintaining it. Great job! |
Homie devices should be better protected from bad data published to
$implementation/ota/payload
(intentionally or unintentionally).Granted, there is some protection in place because we first need to post a version number to
$ota
. However, since$ota
might be retained (the v2 spec allows retaining, homie-ota wisely doesn't), this protection could easily get lost. One bad publish to$implementation/ota/payload
and you have to recover your homie thing from wherever it was installed, hook up a serial port, re-flash it and take it back to wherever it was.The best solution I can imagine would be something along the lines of the flow chart on this website http://www.syncroness.com/portfolio/firmware-updates/ (found it using a quick web search, I'm not associated with that company). Unfortunately, public key infrastructure and RSA decryption might be a bit too heavy here. But, at least a hash or a checksum can easily be run over the firmware binary (that is, skip the key/RSA blocks in the picture). esp8266/Arduino Updater supports MD5 checking. We just need a Homie convention for OTA signatures and some code to enforce them.
For example, a Homie convention could request that the OTA server first publishes a firmware-hash like
md5:098f6bcd4621d373cade4e832627b4f6
to$implementation/ota/signature
and then publishes the actual firmware to$implementation/ota/payload
. By using anmd5:
prefix, the signature checking could later be extended to something more sophisticated. A new config key could specify whether or not a signature is required, and also which signature types are acceptable. That config key would need to be protected against lowering signature requirements.Additional integrity tests could include
Homie_setFirmware
nameI can propose some code for the ESP side and instructions for the server side. Let me know.
The text was updated successfully, but these errors were encountered: