diff --git a/src/SSLClient.h b/src/SSLClient.h index 44baf6d..d12ff56 100644 --- a/src/SSLClient.h +++ b/src/SSLClient.h @@ -112,10 +112,263 @@ public: setTimeout(10 * 1000); } - /* - * The special functions most clients have are below - * Most of them smply pass through + //======================================== + //= Functions implemented in SSLClientImpl + //======================================== + + /** + * @brief Connect over SSL to a host specified by an ip address + * + * SSLClient::connect(host, port) should be preffered over this function, + * as verifying the domain name is a step in ensuring the certificate is + * legitimate, which is important to the security of the device. Additionally, + * SSL sessions cannot be resumed, which can drastically increase initial + * connect time. + * + * This function initializes EthernetClient by calling EthernetClient::connect + * with the parameters supplied, then once the socket is open initializes + * the appropriete bearssl contexts. Due to the design of the SSL standard, + * this function will probably take an extended period (1-4sec) to negotiate + * the handshake and finish the connection. This function runs until the SSL + * handshake succeeds or fails, as found in most Arduino libraries, so be + * sure to design around this in your code. + * + * The implementation for this function can be found in SSLClientImpl::connect(IPAddress, uint16_t) + * + * @pre The underlying client object (passed in through the ctor) in a non- + * error state, and must be able to access the server being connected to. + * @pre SSLCLient can only have one connection at a time, so the client + * object must not already have a socket open. + * @pre There must be sufficient memory availible on the device to verify + * the certificate (if the free memory drops below 8000 bytes during certain + * points in the connection, SSLCLient will fail). + * @pre There must be a trust anchor given to the ctor that corresponds to + * the certificate provided by the IP address being connected to. For more + * information check out the wiki on the pycert-bearssl tool. + * @pre The analog pin passed to the ctor must be set to input, and must + * be wired to something sort of random (floating is fine). + * + * @param ip The ip address to connect to + * @param port the port to connect to + * @returns 1 if success, 0 if failure (as found in EthernetClient) */ + // virtual int connect(IPAddress ip, uint16_t port) = 0; + + /** + * @brief Connect over SSL using connect(ip, port), using a DNS lookup to + * get the IP Address first. + * + * This function initializes EthernetClient by calling EthernetClient::connect + * with the parameters supplied, then once the socket is open uses BearSSL to + * to complete a SSL handshake. This function runs until the SSL handshake + * succeeds or fails, as found in most Arduino libraries. + * + * SSL requires the client to generate some random bits (to be later combined + * with some random bits from the server), so SSLClient uses the least signinigant + * bits from the analog pin supplied in the ctor. The random bits are generated + * from 16 consecutive analogReads, and given to BearSSL before the handshake + * starts. + * + * Due to the design of the SSL standard, this function will probably take an + * extended period (1-4sec) to negotiate the handshake and finish the + * connection. Since the hostname is provided, however, BearSSL is able to keep + * a session cache of the clients we have connected to. This should reduce + * connection time to about 100-200ms. In order to use this feature, the website + * you are connecting to must support it (most do by default), you must + * reuse the same SSLClient object, and you must reconnect to the same server. + * SSLClient automatcally stores an IP address and hostname in each session, + * ensuring that if you call connect("www.google.com") SSLClient will use a + * cached IP address instead of another DNS lookup. Because some websites have + * multiple servers on a single IP address (github.com is an example), however, + * you may find that even if you are connecting to the same host the connection + * does not resume. This is a flaw in the SSL session protocol, and has been + * resolved in future versions. On top of all that, SSL sessions can expire + * based on server criteria, which will result in a regular connection time. + * Because of all these factors, it is generally prudent to assume the + * connection will not be resumed, and go from there. + * + * The implementation for this function can be found in SSLClientImpl::connect(const char*, uint16_t) + * + * @pre The underlying client object (passed in through the ctor) in a non- + * error state, and must be able to access the server being connected to. + * @pre SSLCLient can only have one connection at a time, so the client + * object must not already have a socket open. + * @pre There must be sufficient memory availible on the device to verify + * the certificate (if the free memory drops below 8000 bytes during certain + * points in the connection, SSLCLient will fail). + * @pre There must be a trust anchor given to the ctor that corresponds to + * the certificate provided by the IP address being connected to. For more + * information check out the wiki on the pycert-bearssl tool. + * @pre The analog pin passed to the ctor must be set to input, and must + * be wired to something sort of random (floating is fine). + * + * @param host The cstring host ("www.google.com") + * @param port the port to connect to (443) + * @returns 1 of success, 0 if failure (as found in EthernetClient). + */ + // virtual int connect(const char *host, uint16_t port) = 0; + + /** @see SSLClient::write(uint8_t*, size_t) */ + // virtual size_t write(uint8_t b) = 0; + /** + * @brief Write some bytes to the SSL connection + * + * Assuming all preconditions are met, this function waits for BearSSL + * to be ready for data to be sent, then writes data to the BearSSL IO + * buffer, BUT does not initally send the data. Insead, it is + * then checked if the BearSSL IO buffer is full, and if so, this function + * waits until BearSSL has flushed the buffer (written it to the + * network client) and fills the buffer again. If the function finds + * that the BearSSL buffer is not full, it returns the number of + * bytes written. In other words, this function will only write data + * to the network if the BearSSL IO buffer is full. Instead, you must call + * SSLClient::availible or SSLClient::flush, which will detect that + * the buffer is ready for writing, and will write the data to the network. + * + * This was implemented as a buffered function because users of Arduino Client + * libraries will often write to the network as such: + * @code{.cpp} + * Client client; + * ... + * client.println("GET /asciilogo.txt HTTP/1.1"); + * client.println("Host: arduino.cc"); + * client.println("Connection: close"); + * while (!client.available()) { ... } + * ... + * @endcode + * This is fine with most network clients. With SSL, however, if we are encryting and + * writing to the network every write() call this will result in a lot of + * small encryption tasks. Encryption takes a lot of time and code, and in general + * the larger the batch we can do it in the better. For this reason, write() + * implicitly buffers until SSLClient::availible is called, or until the buffer is full. + * If you would like to trigger a network write manually without using the SSLClient::available, + * you can also call SSLClient::flush, which will write all data and return when finished. + * + * The implementation for this function can be found in SSLClientImpl::write(const uint8_t*, size_t) + * + * @pre The socket and SSL layer must be connected, meaning SSLClient::connected must be true. + * @pre BearSSL must not be waiting for the recipt of user data (if it is, there is + * probably an error with how the protocol in implemented in your code). + * + * @param buf the pointer to a buffer of bytes to copy + * @param size the number of bytes to copy from the buffer + * @returns The number of bytes copied to the buffer (size), or zero if the BearSSL engine + * fails to become ready for writing data. + */ + // virtual size_t write(const uint8_t *buf, size_t size) = 0; + + /** + * @brief Returns the number of bytes availible to read from the SSL Socket + * + * This function updates the state of the SSL engine (including writing any data, + * see SSLClient::write) and as a result should be called periodically when writing + * or expecting data. Additionally, since this function returns zero if there are + * no bytes and if SSLClient::connected is false (this same behavior is found + * in EthernetClient), it is prudent to ensure in your own code that the + * preconditions are met before checking this function to prevent an ambigious + * result. + * + * The implementation for this function can be found in SSLClientImpl::available + * + * @pre SSLClient::connected must be true. + * + * @returns The number of bytes availible (can be zero), or zero if any of the pre + * conditions aren't satisfied. + */ + //virtual int available() = 0; + + /** @see SSLClient::read(uint8_t*, size_t) */ + //virtual int read() = 0; + /** + * @brief Read size bytes from the SSL socket buffer, copying them into *buf, and return the number of bytes read. + * + * This function checks if bytes are ready to be read by calling SSLClient::availible, + * and if so copies size number of bytes from the IO buffer into the buf pointer, and deletes + * that number of bytes from the SSLClient buffer. Data read using this function will not + * include any SSL or socket commands, as the Client and BearSSL will capture those and + * process them seperatley. + * + * It should be noted that a common problem I encountered with SSL connections is + * buffer overflow, caused by the server sending too much data at once. This problem + * is caused by the microcontroller being unable to copy and decrypt data faster + * than it is being recieved, forcing some data to be discarded. This usually puts BearSSL + * in an invalid state in which it is unable to recover, causing SSLClient to close + * the connection with a write error. If you are experiencing frequent timeout problems, + * this could be the reason why. + * + * In order to remedy this problem the device must be able to read the data faster than + * it is being recieved, or have a cache large enough to store the entire recieve payload. + * Since SSL's encryption forces the device to read slowly, this means we must increase + * the cache size. Depending on your platform, there are a number of ways this can be + * done: + * - Sometimes your communication sheild will have an internal buffer, which can be expanded + * through the driver code. This is the case with the Arduino Ethernet library (in the form + * of the MAX_SOCK_NUM and ETHERNET_LARGE_BUFFERS macros), however the library must be + * modified for the change to take effect. + * - SSLClient has an internal buffer SSLClientImpl::m_iobuf, which can be expanded. This will have very + * limited usefulness, however, as BearSSL limits the amount of data that can be processed + * based on the stage in the SSL handshake. + * - If none of the above are viable, it is possible to implement your own Client class which + * has an internal buffer much larger than both the driver and BearSSL. This would require + * in-depth knowlege of programming and the communication shield you are working with. + * Another important question to ask with this problem is: do I need to acsess this website? + * Often times there are other ways to get data that we need that do the same thing, + * and these other ways may offer smaller and more managable response payloads. + * + * The implementation for this function can be found in SSLClientImpl::read(uint8_t*, size_t) + * + * @pre SSLClient::available must be >0 + * + * @param buf The pointer to the buffer to put SSL application data into + * @param size The size (in bytes) to copy to the buffer + * + * @returns The number of bytes copied (<= size), or -1 if the preconditions are not satisfied. + */ + //virtual int read(uint8_t *buf, size_t size) = 0; + + /** + * @brief view the first byte of the buffer, without removing it from the SSLClient Buffer + * The implementation for this function can be found in SSLClientImpl::peek + * @pre SSLClient::available must be >0 + * @returns The first byte recieved, or -1 if the preconditions are not satisfied (warning: + * do not use if your data may be -1, as the return value is ambigious) + */ + //virtual int peek() = 0; + + /** + * @brief Force writing the buffered bytes from SSLClient::write to the network. + * This function is blocking until all bytes from the buffer are written. For + * an explanation of how writing with SSLClient works, please see SSLCLient::write. + * The implementation for this function can be found in SSLClientImpl::flush. + */ + //virtual void flush() = 0; + + /** + * @brief Close the connection + * If the SSL session is still active, all incoming data is discarded and BearSSL will attempt to + * close the session gracefully (will write to the network), and then call m_client::stop. If the session is not active or an + * error was encountered previously, this function will simply call m_client::stop. + * The implementation for this function can be found in SSLClientImpl::peek. + */ + //virtual void stop() = 0; + + /** + * @brief Check if the device is connected. + * Use this function to determine if SSLClient is still connected and a SSL connection is active. + * It should be noted that SSLClient::availible should be prefered over this function for rapid + * polling--both functions send and recieve data to the Client device, however SSLClient::availible + * has some delays built in to protect the Client device from being polled too frequently. + * + * The implementation for this function can be found in SSLClientImpl::connected. + * + * @returns 1 if connected, 0 if not + */ + //virtual uint8_t connected() = 0; + + //======================================== + //= Functions Not in the Client Interface + //======================================== + /** * @brief Equivalent to SSLClient::connected() > 0 * @returns true if connected, false if not diff --git a/src/SSLClientImpl.cpp b/src/SSLClientImpl.cpp index 79f8fd8..515d38f 100644 --- a/src/SSLClientImpl.cpp +++ b/src/SSLClientImpl.cpp @@ -353,7 +353,17 @@ int SSLClientImpl::m_run_until(const unsigned target) { if (state != lastState) { lastState = state; m_info("m_run changed state:", func_name); - printState(state); + if(m_debug == DebugLevel::SSL_INFO) { + m_info("State: ", __func__); + if(state == 0) Serial.println(" Invalid"); + else if (state & BR_SSL_CLOSED) Serial.println(" Connection closed"); + else { + if (state & BR_SSL_SENDREC) Serial.println(" SENDREC"); + if (state & BR_SSL_RECVREC) Serial.println(" RECVREC"); + if (state & BR_SSL_SENDAPP) Serial.println(" SENDAPP"); + if (state & BR_SSL_RECVAPP) Serial.println(" RECVAPP"); + } + } } if (state & BR_SSL_RECVREC) { size_t len; diff --git a/src/SSLClientImpl.h b/src/SSLClientImpl.h index b38ebb3..527783d 100644 --- a/src/SSLClientImpl.h +++ b/src/SSLClientImpl.h @@ -97,188 +97,51 @@ public: explicit SSLClientImpl(Client* client, const br_x509_trust_anchor *trust_anchors, const size_t trust_anchors_num, const int analog_pin, const DebugLevel debug); - /* functions dealing with read/write that BearSSL will be injected into */ - /** - * @brief Connect over SSL to a host specified by an ip address - * - * SSLClient::connect(host, port) should be preffered over this function, - * as verifying the domain name is a step in ensuring the certificate is - * legitimate, which is important to the security of the device. Additionally, - * SSL sessions cannot be resumed, which can drastically increase initial - * connect time. - * - * This function initializes EthernetClient by calling EthernetClient::connect - * with the parameters supplied, then once the socket is open initializes - * the appropriete bearssl contexts. Due to the design of the SSL standard, - * this function will probably take an extended period (1-4sec) to negotiate - * the handshake and finish the connection. This function runs until the SSL - * handshake succeeds or fails, as found in most Arduino libraries, so be - * sure to design around this in your code. - * - * @pre The underlying client object (passed in through the ctor) in a non- - * error state, and must be able to access the server being connected to. - * @pre SSLCLient can only have one connection at a time, so the client - * object must not already have a socket open. - * @pre There must be sufficient memory availible on the device to verify - * the certificate (if the free memory drops below 8000 bytes during certain - * points in the connection, SSLCLient will fail). - * @pre There must be a trust anchor given to the ctor that corresponds to - * the certificate provided by the IP address being connected to. For more - * information check out the wiki on the pycert-bearssl tool. - * @pre The analog pin passed to the ctor must be set to input, and must - * be wired to something sort of random (floating is fine). - * - * @param ip The ip address to connect to - * @param port the port to connect to - * @returns 1 if success, 0 if failure (as found in EthernetClient) - */ + //============================================ + //= Functions implemented in SSLClientImpl.cpp + //============================================ + + /** @see SSLClient::connect(IPAddress, uint16_t) */ virtual int connect(IPAddress ip, uint16_t port); - - /** - * @brief Connect over SSL using connect(ip, port), using a DNS lookup to - * get the IP Address first. - * - * This function initializes EthernetClient by calling EthernetClient::connect - * with the parameters supplied, then once the socket is open uses BearSSL to - * to complete a SSL handshake. This function runs until the SSL handshake - * succeeds or fails, as found in most Arduino libraries. - * - * SSL requires the client to generate some random bits (to be later combined - * with some random bits from the server), so SSLClient uses the least signinigant - * bits from the analog pin supplied in the ctor. The random bits are generated - * from 16 consecutive analogReads, and given to BearSSL before the handshake - * starts. - * - * Due to the design of the SSL standard, this function will probably take an - * extended period (1-4sec) to negotiate the handshake and finish the - * connection. Since the hostname is provided, however, BearSSL is able to keep - * a session cache of the clients we have connected to. This should reduce - * connection time to about 100-200ms. In order to use this feature, the website - * you are connecting to must support it (most do by default), you must - * reuse the same SSLClient object, and you must reconnect to the same server. - * SSLClient automatcally stores an IP address and hostname in each session, - * ensuring that if you call connect("www.google.com") SSLClient will use a - * cached IP address instead of another DNS lookup. Because some websites have - * multiple servers on a single IP address (github.com is an example), however, - * you may find that even if you are connecting to the same host the connection - * does not resume. This is a flaw in the SSL session protocol, and has been - * resolved in future versions. On top of all that, SSL sessions can expire - * based on server criteria, which will result in a regular connection time. - * Because of all these factors, it is generally prudent to assume the - * connection will not be resumed, and go from there. - * - * @pre The underlying client object (passed in through the ctor) in a non- - * error state, and must be able to access the server being connected to. - * @pre SSLCLient can only have one connection at a time, so the client - * object must not already have a socket open. - * @pre There must be sufficient memory availible on the device to verify - * the certificate (if the free memory drops below 8000 bytes during certain - * points in the connection, SSLCLient will fail). - * @pre There must be a trust anchor given to the ctor that corresponds to - * the certificate provided by the IP address being connected to. For more - * information check out the wiki on the pycert-bearssl tool. - * @pre The analog pin passed to the ctor must be set to input, and must - * be wired to something sort of random (floating is fine). - * - * @param host The cstring host ("www.google.com") - * @param port the port to connect to (443) - * @returns 1 of success, 0 if failure (as found in EthernetClient). - */ + /** @see SSLClient::connect(const char*, uint16_t) */ virtual int connect(const char *host, uint16_t port); - - /** @see SSLClient::write(uint8_t*, size_t) */ + /** @see SSLClient::write(const uint8_t*, size_t) */ virtual size_t write(uint8_t b) { return write(&b, 1); } - /** - * @brief Write some bytes to the SSL connection - * - * Assuming all preconditions are met, this function waits for BearSSL - * to be ready for data to be sent, then writes data to the BearSSL IO - * buffer, BUT does not initally send the data. Insead, it is - * then checked if the BearSSL IO buffer is full, and if so, this function - * waits until BearSSL has flushed the buffer (written it to the - * network client) and fills the buffer again. If the function finds - * that the BearSSL buffer is not full, it returns the number of - * bytes written. In other words, this function will only write data - * to the network if the BearSSL IO buffer is full. Instead, you must call - * SSLClient::availible or SSLClient::flush, which will detect that - * the buffer is ready for writing, and will write the data to the network. - * - * This was implemented as a buffered function because users of Arduino Client - * libraries will often write to the network as such: - * @code{.cpp} - * Client client; - * ... - * client.println("GET /asciilogo.txt HTTP/1.1"); - * client.println("Host: arduino.cc"); - * client.println("Connection: close"); - * while (!client.available()) { ... } - * ... - * @endcode - * This is fine with most network clients. With SSL, however, if we are encryting and - * writing to the network every write() call this will result in a lot of - * small encryption tasks. Encryption takes a lot of time and code, and in general - * the larger the batch we can do it in the better. For this reason, write() - * implicitly buffers until SSLClient::availible is called, or until the buffer is full. - * If you would like to trigger a network write manually without using the SSLClient::available, - * you can also call SSLClient::flush, which will write all data and return when finished. - * - * @pre The socket and SSL layer must be connected, meaning SSLClient::connected must be true. - * @pre BearSSL must not be waiting for the recipt of user data (if it is, there is - * probably an error with how the protocol in implemented in your code). - * - * @param buf the pointer to a buffer of bytes to copy - * @param size the number of bytes to copy from the buffer - * @returns The number of bytes copied to the buffer (size), or zero if the BearSSL engine - * fails to become ready for writing data. - */ + /** @see SSLClient::write(const uint8_t*, size_t) */ virtual size_t write(const uint8_t *buf, size_t size); - - /** - * @brief Returns the number of bytes availible to read from the SSL Socket - * - * This function updates the state of the SSL engine (including writing any data, - * see SSLClient::write) and as a result should be called periodically when writing - * or expecting data. Additionally, since this function returns zero if there are - * no bytes and if SSLClient::connected is false (this same behavior is found - * in EthernetClient), it is prudent to ensure in your own code that the - * preconditions are met before checking this function to prevent an ambigious - * result. - * - * @pre SSLClient::connected must be true. - * - * @returns The number of bytes availible (can be zero), or zero if any of the pre - * conditions aren't satisfied. - */ + /** @see SSLClient::available */ virtual int available(); - /** @see SSLClient::read(uint8_t*, size_t) */ virtual int read() { uint8_t read_val; return read(&read_val, 1) > 0 ? read_val : -1; } - /** - * @brief Read size bytes from the SSL socket buffer, copying them into *buf, and return the number of bytes read. - * - * This function checks if bytes are ready to be read by calling SSLClient::availible, - * and if there are some copies size number of bytes from the IO buffer into buf. - * - * It should be noted that a common problem I encountered with SSL connections is - * buffer overflow, caused by the server sending too much data at once. This problem - * only occurs... - * - * TODO: finish - */ + /** @see SSLClient::read(uint8_t*, size_t) */ virtual int read(uint8_t *buf, size_t size); + /** @see SSLClient::peek */ virtual int peek(); + /** @see SSLClient::flush */ virtual void flush(); + /** @see SSLClient::stop */ virtual void stop(); + /** @see SSLClient::connected */ virtual uint8_t connected(); - // stub virtual functions to get things from the client + //============================================ + //= Functions implemented in SSLClient.h + //============================================ + /** See SSLClient::localPort */ virtual uint16_t localPort() = 0; + /** See SSLClient::remoteIP */ virtual IPAddress remoteIP() = 0; + /** See SSLClient::localPort */ virtual uint16_t remotePort() = 0; - - // as well as store and retrieve session data + /** See SSLClient::getSession */ virtual SSLSession& getSession(const char* host, const IPAddress& addr) = 0; + protected: + + //============================================ + //= Functions implemented in SSLClientImpl.cpp + //============================================ + /** * @brief set the pointer to the Client class that we wil use * @@ -319,19 +182,6 @@ protected: void m_error(const T str, const char* func_name) const { m_print(str, func_name, SSL_ERROR); } private: - void printState(unsigned state) const { - if(m_debug == DebugLevel::SSL_INFO) { - m_info("State: ", __func__); - if(state == 0) Serial.println(" Invalid"); - else if (state & BR_SSL_CLOSED) Serial.println(" Connection closed"); - else { - if (state & BR_SSL_SENDREC) Serial.println(" SENDREC"); - if (state & BR_SSL_RECVREC) Serial.println(" RECVREC"); - if (state & BR_SSL_SENDAPP) Serial.println(" SENDAPP"); - if (state & BR_SSL_RECVAPP) Serial.println(" RECVAPP"); - } - } - } /** Returns whether or not the engine is connected, without polling the client over SPI or other (as opposed to connected()) */ bool m_soft_connected(const char* func_name); /** start the ssl engine on the connected client */ @@ -340,6 +190,11 @@ private: int m_run_until(const unsigned target); /** proxy for availble that returns the state */ unsigned m_update_engine(); + + //============================================ + //= Data Members + //============================================ + // hold a reference to the client Client* m_client; // store pointers to the trust anchors @@ -359,6 +214,13 @@ private: // simply edit this value to change the buffer size to the desired value // additionally, we need to correct buffer size based off of how many sessions we decide to cache // since SSL takes so much memory if we don't it will cause the stack and heap to collide + /** + * @brief The internal buffer to use with BearSSL. + * This buffer controls how much data BearSSL can encrypt/decrypt at a given time. It can be expanded + * or shrunk to [255, BR_SSL_BUFSIZE_BIDI], depending on the memory and speed needs of your application. + * As a rule of thumb SSLClient will fail if it does not have at least 8000 bytes when starting a + * connection. + */ unsigned char m_iobuf[BR_SSL_BUFSIZE_MONO / 4]; static_assert(sizeof m_iobuf <= BR_SSL_BUFSIZE_BIDI, "m_iobuf must be below maximum buffer size"); // store the index of where we are writing in the buffer