|
| 1 | +/* For any secure connection - it is (at least) essential for the |
| 2 | + the client to verify that it is talking with the server it |
| 3 | + thinks it is talking to. And not some (invisible) man in the middle. |
| 4 | +
|
| 5 | + See https://en.wikipedia.org/wiki/Man-in-the-middle_attack, |
| 6 | + https://www.ai.rug.nl/mas/finishedprojects/2011/TLS/hermsencomputerservices.nl/mas/mitm.html or |
| 7 | + https://medium.com/@munteanu210/ssl-certificates-vs-man-in-the-middle-attacks-3fb7846fa5db |
| 8 | + for some background on this. |
| 9 | +
|
| 10 | + Unfortunatley this means that one needs to hardcode a server |
| 11 | + public key, certificate or some cryptographically strong hash |
| 12 | + thereoff into the code, to verify that you are indeed talking to |
| 13 | + the right server. This is sometimes somewhat impractical. Especially |
| 14 | + if you do not know the server in advance; or if your code needs to be |
| 15 | + stable ovr very long times - during which the server may change. |
| 16 | +
|
| 17 | + However completely dispensing with any checks (See the WifiClientInSecure |
| 18 | + example) is also not a good idea either. |
| 19 | +
|
| 20 | + This example gives you some middle ground; "Trust on First Use" -- |
| 21 | + TOFU - see https://developer.mozilla.org/en-US/docs/Glossary/TOFU or |
| 22 | + https://en.wikipedia.org/wiki/Trust_on_first_use). |
| 23 | +
|
| 24 | + In this scheme; we start the very first time without any security checks |
| 25 | + but once we have our first connection; we store the public crytpographic |
| 26 | + details (or a proxy, such as a sha256 of this). And then we use this for |
| 27 | + any subsequent connections. |
| 28 | +
|
| 29 | + The assumption here is that we do our very first connection in a somewhat |
| 30 | + trusted network environment; where the chance of a man in the middle is |
| 31 | + very low; or one where the person doing the first run can check the |
| 32 | + details manually. |
| 33 | +
|
| 34 | + So this is not quite as good as building a CA certificate into your |
| 35 | + code (as per the WifiClientSecure example). But not as bad as something |
| 36 | + with no trust management at all. |
| 37 | +
|
| 38 | + To make it possible for the enduser to 'reset' this trust; the |
| 39 | + startup sequence checks if a certain GPIO is low (assumed to be wired |
| 40 | + to some physical button or jumper on the PCB). And we only allow |
| 41 | + the TOFU to be configured when this pin is LOW. |
| 42 | +*/ |
| 43 | +#ifndef WIFI_NETWORK |
| 44 | +#define WIFI_NETWORK "Your Wifi SSID" |
| 45 | +#endif |
| 46 | + |
| 47 | +#ifndef WIFI_PASSWD |
| 48 | +#define WIFI_PASSWD "your-secret-wifi-password" |
| 49 | +#endif |
| 50 | + |
| 51 | +const char* ssid = WIFI_NETWORK; // your network SSID (name of wifi network) |
| 52 | +const char* password = WIFI_PASSWD; // your network password |
| 53 | +const char* server = "www.howsmyssl.com"; // Server to test with. |
| 54 | + |
| 55 | +const int TOFU_RESET_BUTTON = 35; /* Trust reset button wired between GPIO 35 and GND (pulldown) */ |
| 56 | + |
| 57 | +#include <WiFiClientSecure.h> |
| 58 | +#include <EEPROM.h> |
| 59 | + |
| 60 | +/* Set aside some persistant memory (i.e. memory that is preserved on reboots and |
| 61 | + power cycling; and will generally survive software updates as well. |
| 62 | +*/ |
| 63 | +EEPROMClass TOFU("tofu0"); |
| 64 | + |
| 65 | +// Utility function; checks if a given buffer is entirly |
| 66 | +// with with 0 bytes over its full length. Returns 0 on |
| 67 | +// succes; a non zero value on fail. |
| 68 | +// |
| 69 | +static int memcmpzero(unsigned char * ptr, size_t len) { |
| 70 | + while (len--) if (0xff != *ptr++) return -1; |
| 71 | + return 0; |
| 72 | +}; |
| 73 | + |
| 74 | +static void printSHA256(unsigned char * ptr) { |
| 75 | + for (int i = 0; i < 32; i++) Serial.printf("%s%02x", i ? ":" : "", ptr[i]); |
| 76 | + Serial.println(""); |
| 77 | +}; |
| 78 | + |
| 79 | +WiFiClientSecure client; |
| 80 | + |
| 81 | +bool get_tofu(); |
| 82 | +bool doTOFU_Protected_Connection(uint8_t * fingerprint_tofu); |
| 83 | + |
| 84 | +void setup() { |
| 85 | + bool tofu_reset = false; |
| 86 | + //Initialize serial and wait for port to open: |
| 87 | + Serial.begin(115200); |
| 88 | + delay(100); |
| 89 | + |
| 90 | + if (!TOFU.begin(32)) { |
| 91 | + Serial.println("Could not initialsize the EEPROM"); |
| 92 | + return; |
| 93 | + } |
| 94 | + uint8_t fingerprint_tofu[32]; |
| 95 | + |
| 96 | + // reset the trust if the tofu reset button is pressed. |
| 97 | + // |
| 98 | + pinMode(TOFU_RESET_BUTTON, INPUT_PULLUP); |
| 99 | + if (digitalRead(TOFU_RESET_BUTTON) == LOW) { |
| 100 | + Serial.println("The TOFU reset button is pressed."); |
| 101 | + tofu_reset = true; |
| 102 | + } |
| 103 | + /* if the button is not pressed; see if we can get the TOFU |
| 104 | + fingerprint from the EEPROM. |
| 105 | + */ |
| 106 | + else if (32 != TOFU.readBytes(0, fingerprint_tofu, 32)) { |
| 107 | + Serial.println("Failed to get the fingerprint from memory."); |
| 108 | + tofu_reset = true; |
| 109 | + } |
| 110 | + /* And check that the EEPROM value is not all 0's; in which |
| 111 | + case we also need to do a TOFU. |
| 112 | + */ |
| 113 | + else if (!memcmpzero(fingerprint_tofu, 32)) { |
| 114 | + Serial.println("TOFU fingerprint in memory all zero."); |
| 115 | + tofu_reset = true; |
| 116 | + }; |
| 117 | + if (!tofu_reset) { |
| 118 | + Serial.print("TOFU pegged to fingerprint: SHA256="); |
| 119 | + printSHA256(fingerprint_tofu); |
| 120 | + Serial.print("Note: You can check this fingerprint by going to the URL\n" |
| 121 | + "<https://"); |
| 122 | + Serial.print(server); |
| 123 | + Serial.println("> and then click on the lock icon.\n"); |
| 124 | + }; |
| 125 | + |
| 126 | + // attempt to connect to Wifi network: |
| 127 | + Serial.print("Attempting to connect to SSID: "); |
| 128 | + Serial.println(ssid); |
| 129 | + WiFi.begin(ssid, password); |
| 130 | + while (WiFi.status() != WL_CONNECTED) { |
| 131 | + Serial.print("."); |
| 132 | + // wait 1 second for re-trying |
| 133 | + delay(1000); |
| 134 | + } |
| 135 | + |
| 136 | + Serial.print("Connected to "); |
| 137 | + Serial.println(ssid); |
| 138 | + |
| 139 | + if (tofu_reset) { |
| 140 | + Serial.println("Resetting trust fingerprint."); |
| 141 | + if (!get_tofu()) { |
| 142 | + Serial.println("Trust reset failed. Giving up"); |
| 143 | + return; |
| 144 | + } |
| 145 | + Serial.println("(New) Trust of First used configured. Rebooting in 3 seconds"); |
| 146 | + delay(3 * 1000); |
| 147 | + ESP.restart(); |
| 148 | + }; |
| 149 | + |
| 150 | + Serial.println("Trying to connect to a server; using TOFU details from the eeprom"); |
| 151 | + |
| 152 | + if (doTOFU_Protected_Connection(fingerprint_tofu)) |
| 153 | + Serial.println("ALL OK"); |
| 154 | +} |
| 155 | + |
| 156 | +bool get_tofu() { |
| 157 | + Serial.println("\nStarting our insecure connection to server..."); |
| 158 | + client.setInsecure();//skip verification |
| 159 | + |
| 160 | + if (!client.connect(server, 443)) { |
| 161 | + Serial.println("Connection failed!"); |
| 162 | + client.stop(); |
| 163 | + return false; |
| 164 | + }; |
| 165 | + |
| 166 | + Serial.println("Connected to server. Extracting trust data."); |
| 167 | + |
| 168 | + // Now extract the data of the certificate and show it to |
| 169 | + // the user over the serial connection for optional |
| 170 | + // verification. |
| 171 | + const mbedtls_x509_crt* peer = client.getPeerCertificate(); |
| 172 | + char buf[1024]; |
| 173 | + int l = mbedtls_x509_crt_info(buf, sizeof(buf), "", peer); |
| 174 | + if (l <= 0) { |
| 175 | + Serial.println("Peer conversion to printable buffer failed"); |
| 176 | + client.stop(); |
| 177 | + return false; |
| 178 | + }; |
| 179 | + Serial.println(); |
| 180 | + Serial.println(buf); |
| 181 | + |
| 182 | + // Extract the fingerprint - and store this in our EEPROM |
| 183 | + // to be used for future validation. |
| 184 | + |
| 185 | + uint8_t fingerprint_remote[32]; |
| 186 | + if (!client.getFingerprintSHA256(fingerprint_remote)) { |
| 187 | + Serial.println("Failed to get the fingerprint"); |
| 188 | + client.stop(); |
| 189 | + return false; |
| 190 | + } |
| 191 | + if ( |
| 192 | + (32 != TOFU.writeBytes(0, fingerprint_remote, 32)) || |
| 193 | + (!TOFU.commit()) |
| 194 | + ) { |
| 195 | + Serial.println("Could not write the fingerprint to the EEPROM"); |
| 196 | + client.stop(); |
| 197 | + return false; |
| 198 | + }; |
| 199 | + TOFU.end(); |
| 200 | + client.stop(); |
| 201 | + |
| 202 | + Serial.print("TOFU pegged to fingerprint: SHA256="); |
| 203 | + printSHA256(fingerprint_remote); |
| 204 | + |
| 205 | + return true; |
| 206 | +}; |
| 207 | + |
| 208 | +bool doTOFU_Protected_Connection(uint8_t * fingerprint_tofu) { |
| 209 | + |
| 210 | + // As we're not using a (CA) certificate to check the |
| 211 | + // connection; but the hash of the peer - we need to initially |
| 212 | + // allow the connection to be set up without the CA check. |
| 213 | + client.setInsecure();//skip verification |
| 214 | + |
| 215 | + if (!client.connect(server, 443)) { |
| 216 | + Serial.println("Connection failed!"); |
| 217 | + client.stop(); |
| 218 | + return false; |
| 219 | + }; |
| 220 | + |
| 221 | + // Now that we're connected - we can check that we have |
| 222 | + // end to end trust - by comparing the fingerprint we (now) |
| 223 | + // see (of the server certificate) to the one we have stored |
| 224 | + // in our EEPROM as part of an earlier trust-on-first use. |
| 225 | + uint8_t fingerprint_remote[32]; |
| 226 | + if (!client.getFingerprintSHA256(fingerprint_remote)) { |
| 227 | + Serial.println("Failed to get the fingerprint of the server"); |
| 228 | + client.stop(); |
| 229 | + return false; |
| 230 | + } |
| 231 | + if (memcmp(fingerprint_remote, fingerprint_tofu, 32)) { |
| 232 | + Serial.println("TOFU fingerprint not the same as the one from the server."); |
| 233 | + Serial.print("TOFU : SHA256="); |
| 234 | + printSHA256(fingerprint_tofu); |
| 235 | + Serial.print("Remote: SHA256="); |
| 236 | + printSHA256(fingerprint_remote); |
| 237 | + Serial.println(" : NOT identical -- Aborting!"); |
| 238 | + client.stop(); |
| 239 | + return false; |
| 240 | + }; |
| 241 | + |
| 242 | + Serial.println("All well - you are talking to the same server as\n" |
| 243 | + "when you set up TOFU. So we can now do a GET.\n\n"); |
| 244 | + |
| 245 | + client.println("GET /a/check HTTP/1.0"); |
| 246 | + client.print("Host: " ); client.println(server); |
| 247 | + client.println("Connection: close"); |
| 248 | + client.println(); |
| 249 | + |
| 250 | + bool inhdr = true; |
| 251 | + while (client.connected()) { |
| 252 | + String line = client.readStringUntil('\n'); |
| 253 | + Serial.println(line); |
| 254 | + if (inhdr && line == "\r") { |
| 255 | + inhdr = false; |
| 256 | + Serial.println("-- headers received. Payload follows\n\n"); |
| 257 | + } |
| 258 | + } |
| 259 | + Serial.println("\n\n-- Payload ended."); |
| 260 | + client.stop(); |
| 261 | + return true; |
| 262 | +} |
| 263 | + |
| 264 | +void loop() {} |
0 commit comments