diff --git a/src/Config.h b/src/Config.h index 5f64dce..cb8f868 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-14" +#define NUKI_HUB_DATE "2025-02-16" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 77d492f..70256f0 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -9,6 +9,7 @@ #include "esp_random.h" #ifdef CONFIG_SOC_SPIRAM_SUPPORTED #include "esp_psram.h" +#include "util/SSLCert.hpp" #endif #ifndef CONFIG_IDF_TARGET_ESP32H2 #include @@ -774,6 +775,10 @@ void WebCfgServer::initialize() { return buildHttpSSLConfigHtml(request, resp, 2); } + else if (value == "selfsignhttps") + { + return buildHttpSSLConfigHtml(request, resp, 3); + } else if (value == "nukicfg") { return buildNukiConfigHtml(request, resp); @@ -2727,6 +2732,13 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "HTTPGEN") + { + createSSLCertificate(); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } #endif else if(key == "UPTIME") { @@ -5097,6 +5109,7 @@ esp_err_t WebCfgServer::buildNetworkConfigHtml(PsychicRequest *request, PsychicR { response.print("Set HTTP SSL Certificate"); response.print("Set HTTP SSL Key"); + response.print("Generate self-signed HTTP SSL Certificate and key"); printInputField(&response, "HTTPSFQDN", "Nuki Hub FQDN for HTTP redirect", _preferences->getString(preference_https_fqdn, "").c_str(), 255, ""); } #endif @@ -5326,7 +5339,7 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR printTextarea(&response, "HTTPCRT", "HTTP SSL Certificate (*, optional)", "", 4400, true, true); } } - else + else if (type == 2) { bool found = false; @@ -5360,6 +5373,11 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR printTextarea(&response, "HTTPKEY", "HTTP SSL Key (*, optional)", "", 2200, true, true); } } + else + { + response.print(""); + response.print("Click save to generate a HTTPS SSL Certificate and key"); + } response.print(""); response.print("
"); response.print(""); @@ -7005,4 +7023,64 @@ const String WebCfgServer::getPreselectionForGpio(const uint8_t &pin) const return String((int8_t)PinRole::Disabled); } + +#ifdef CONFIG_SOC_SPIRAM_SUPPORTED +void WebCfgServer::createSSLCertificate() +{ + SSLCert* cert; + cert = new SSLCert(); + int createCertResult = createSelfSignedCert( + *cert, + KEYSIZE_2048, + "CN=nukihub.local,O=NukiHub,C=DE", + "20250101000000", + "20350101000000" + ); + bool crtSuccess = false; + bool keySuccess = false; + + if (createCertResult == 0) { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/http_ssl.crt", FILE_WRITE); + if (!file) { + Log->println("Failed to open /http_ssl.crt for writing"); + } + else + { + if (!file.write((byte *)cert->getCertData(), cert->getCertLength())) + { + Log->println("Failed to write /http_ssl.crt"); + } + else + { + crtSuccess = true; + } + file.close(); + } + + File file2 = SPIFFS.open("/http_ssl.key", FILE_WRITE); + if (!file2) { + Log->println("Failed to open /http_ssl.key for writing"); + } + else + { + if (!file2.write((byte *)cert->getPKData(), cert->getPKLength())) + { + Log->println("Failed to write /http_ssl.key"); + } + else + { + keySuccess = true; + } + file2.close(); + } + } + } +} +#endif + #endif diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 04a7224..f32a06e 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -86,7 +86,9 @@ private: #if defined(CONFIG_IDF_TARGET_ESP32) const std::vector> getNetworkCustomCLKOptions() const; #endif - + #ifdef CONFIG_SOC_SPIRAM_SUPPORTED + void createSSLCertificate(); + #endif const String getPreselectionForGpio(const uint8_t& pin) const; const String pinStateToString(const NukiPinState& value) const; diff --git a/src/util/SSLCert.cpp b/src/util/SSLCert.cpp new file mode 100644 index 0000000..df99128 --- /dev/null +++ b/src/util/SSLCert.cpp @@ -0,0 +1,343 @@ +#include "SSLCert.hpp" + +SSLCert::SSLCert(unsigned char * certData, uint16_t certLength, unsigned char * pkData, uint16_t pkLength): + _certLength(certLength), + _certData(certData), + _pkLength(pkLength), + _pkData(pkData) { + +} + +SSLCert::~SSLCert() { + // TODO Auto-generated destructor stub +} + + +uint16_t SSLCert::getCertLength() { + return _certLength; +} + +uint16_t SSLCert::getPKLength() { + return _pkLength; +} + +unsigned char * SSLCert::getCertData() { + return _certData; +} + +unsigned char * SSLCert::getPKData() { + return _pkData; +} + +void SSLCert::setPK(unsigned char * pkData, uint16_t length) { + _pkData = pkData; + _pkLength = length; +} + +void SSLCert::setCert(unsigned char * certData, uint16_t length) { + _certData = certData; + _certLength = length; +} + +void SSLCert::clear() { + for(uint16_t i = 0; i < _certLength; i++) _certData[i]=0; + delete _certData; + _certLength = 0; + + for(uint16_t i = 0; i < _pkLength; i++) _pkData[i] = 0; + delete _pkData; + _pkLength = 0; +} + +/** + * Function to create the key for a self-signed certificate. + * + * Writes private key as DER in certCtx + * + * Based on programs/pkey/gen_key.c + */ +static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { + + // Initialize the entropy source + mbedtls_entropy_context entropy; + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ctr_drbg_init( &ctr_drbg ); + int rngRes = mbedtls_ctr_drbg_seed( + &ctr_drbg, mbedtls_entropy_func, &entropy, + NULL, 0 + ); + if (rngRes != 0) { + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_RNG; + } + + // Initialize the private key + mbedtls_pk_context key; + mbedtls_pk_init( &key ); + int resPkSetup = mbedtls_pk_setup( &key, mbedtls_pk_info_from_type( MBEDTLS_PK_RSA ) ); + if ( resPkSetup != 0) { + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK; + } + + // Actual key generation + int resPkGen = mbedtls_rsa_gen_key( + mbedtls_pk_rsa( key ), + mbedtls_ctr_drbg_random, + &ctr_drbg, + keySize, + 65537 + ); + if ( resPkGen != 0) { + mbedtls_pk_free( &key ); + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_GEN_PK; + } + + // Free the entropy source and the RNG as they are no longer needed + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + + // Allocate the space on the heap, as stack size is quite limited + unsigned char * output_buf = new unsigned char[4096]; + if (output_buf == NULL) { + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM; + } + memset(output_buf, 0, 4096); + + // Write the key to the temporary buffer and determine its length + int resPkWrite = mbedtls_pk_write_key_der( &key, output_buf, 4096 ); + if (resPkWrite < 0) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + size_t pkLength = resPkWrite; + unsigned char *pkOffset = output_buf + sizeof(unsigned char) * 4096 - pkLength; + + // Copy the key into a new, fitting space on the heap + unsigned char * output_pk = new unsigned char[pkLength]; + if (output_pk == NULL) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + memcpy(output_pk, pkOffset, pkLength); + + // Clean up the temporary buffer and clear the key context + delete[] output_buf; + mbedtls_pk_free( &key ); + + // Set the private key in the context + certCtx.setPK(output_pk, pkLength); + + return 0; +} + +static int parse_serial_decimal_format(unsigned char *obuf, size_t obufmax, + const char *ibuf, size_t *len) +{ + unsigned long long int dec; + unsigned int remaining_bytes = sizeof(dec); + unsigned char *p = obuf; + unsigned char val; + char *end_ptr = NULL; + + errno = 0; + dec = strtoull(ibuf, &end_ptr, 10); + + if ((errno != 0) || (end_ptr == ibuf)) { + return -1; + } + + *len = 0; + + while (remaining_bytes > 0) { + if (obufmax < (*len + 1)) { + return -1; + } + + val = (dec >> ((remaining_bytes - 1) * 8)) & 0xFF; + + /* Skip leading zeros */ + if ((val != 0) || (*len != 0)) { + *p = val; + (*len)++; + p++; + } + + remaining_bytes--; + } + + return 0; +} + +/** + * Function to generate the X509 certificate data for a private key + * + * Writes certificate in certCtx + * + * Based on programs/x509/cert_write.c + */ + +static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom, std::string validityTo) { + int funcRes = 0; + int stepRes = 0; + + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_pk_context key; + mbedtls_x509write_cert crt; + unsigned char * primary_buffer; + unsigned char *certOffset; + unsigned char * output_buffer; + size_t certLength; + const char *defaultSerial = "1"; + unsigned char serial[MBEDTLS_X509_RFC5280_MAX_SERIAL_LEN]; + size_t serial_len; + + // Make a C-friendly version of the distinguished name + char dn_cstr[dn.length()+1]; + strcpy(dn_cstr, dn.c_str()); + + // Initialize the entropy source + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_init( &ctr_drbg ); + stepRes = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_RNG; + goto error_after_entropy; + } + + mbedtls_pk_init( &key ); + stepRes = mbedtls_pk_parse_key( &key, certCtx.getPKData(), certCtx.getPKLength(), NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_READKEY; + goto error_after_key; + } + + // Start configuring the certificate + mbedtls_x509write_crt_init( &crt ); + // Set version and hash algorithm + mbedtls_x509write_crt_set_version( &crt, MBEDTLS_X509_CRT_VERSION_3 ); + mbedtls_x509write_crt_set_md_alg( &crt, MBEDTLS_MD_SHA256 ); + + // Set the keys (same key as we self-sign) + mbedtls_x509write_crt_set_subject_key( &crt, &key ); + mbedtls_x509write_crt_set_issuer_key( &crt, &key ); + + // Set issuer and subject (same, as we self-sign) + stepRes = mbedtls_x509write_crt_set_subject_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + stepRes = mbedtls_x509write_crt_set_issuer_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + + // Set the validity of the certificate. At the moment, it's fixed from 2019 to end of 2029. + stepRes = mbedtls_x509write_crt_set_validity( &crt, validityFrom.c_str(), validityTo.c_str()); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Make this a CA certificate + stepRes = mbedtls_x509write_crt_set_basic_constraints( &crt, 1, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Initialize the serial number + stepRes = parse_serial_decimal_format(serial, sizeof(serial), defaultSerial, &serial_len); + + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + + stepRes = mbedtls_x509write_crt_set_serial_raw( &crt, serial, sizeof(serial) ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + + // Create buffer to write the certificate + primary_buffer = new unsigned char[4096]; + if (primary_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_cert_serial; + } + + // Write the actual certificate + stepRes = mbedtls_x509write_crt_der(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg ); + if (stepRes < 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_WRITE; + goto error_after_primary_buffer; + } + + // Create a matching buffer + certLength = stepRes; + certOffset = primary_buffer + sizeof(unsigned char) * 4096 - certLength; + + // Copy the cert into a new, fitting space on the heap + output_buffer = new unsigned char[certLength]; + if (output_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_primary_buffer; + } + memcpy(output_buffer, certOffset, certLength); + + // Configure the cert in the context + certCtx.setCert(output_buffer, certLength); + + // Run through the cleanup process +error_after_primary_buffer: + delete[] primary_buffer; + +error_after_cert_serial: + +error_after_cert: + mbedtls_x509write_crt_free( &crt ); + +error_after_key: + mbedtls_pk_free(&key); + +error_after_entropy: + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return funcRes; +} + +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom, std::string validUntil) { + + // Add the private key + int keyRes = gen_key(certCtx, keySize); + if (keyRes != 0) { + // Key-generation failed, return the failure code + return keyRes; + } + + // Add the self-signed certificate + int certRes = cert_write(certCtx, dn, validFrom, validUntil); + if (certRes != 0) { + // Cert writing failed, reset the pk and return failure code + certCtx.setPK(NULL, 0); + return certRes; + } + + // If all went well, return 0 + return 0; +} \ No newline at end of file diff --git a/src/util/SSLCert.hpp b/src/util/SSLCert.hpp new file mode 100644 index 0000000..8860d56 --- /dev/null +++ b/src/util/SSLCert.hpp @@ -0,0 +1,171 @@ +#ifndef SRC_SSLCERT_HPP_ +#define SRC_SSLCERT_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HTTPS_SERVER_ERROR_KEYGEN 0x0F +#define HTTPS_SERVER_ERROR_KEYGEN_RNG 0x02 +#define HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK 0x03 +#define HTTPS_SERVER_ERROR_KEYGEN_GEN_PK 0x04 +#define HTTPS_SERVER_ERROR_KEY_WRITE_PK 0x05 +#define HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM 0x06 +#define HTTPS_SERVER_ERROR_CERTGEN 0x1F +#define HTTPS_SERVER_ERROR_CERTGEN_RNG 0x12 +#define HTTPS_SERVER_ERROR_CERTGEN_READKEY 0x13 +#define HTTPS_SERVER_ERROR_CERTGEN_WRITE 0x15 +#define HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM 0x16 +#define HTTPS_SERVER_ERROR_CERTGEN_NAME 0x17 +#define HTTPS_SERVER_ERROR_CERTGEN_SERIAL 0x18 +#define HTTPS_SERVER_ERROR_CERTGEN_VALIDITY 0x19 + +/** + * \brief Certificate and private key that can be passed to the HTTPSServer. + * + * **Converting PEM to DER Files** + * + * Certificate: + * ```bash + * openssl x509 -inform PEM -outform DER -in myCert.crt -out cert.der + * ``` + * + * Private Key: + * ```bash + * openssl rsa -inform PEM -outform DER -in myCert.key -out key.der + * ``` + * + * **Converting DER File to C Header** + * + * ```bash + * echo "#ifndef KEY_H_" > ./key.h + * echo "#define KEY_H_" >> ./key.h + * xxd -i key.der >> ./key.h + * echo "#endif" >> ./key.h + * ``` + */ +class SSLCert { +public: + /** + * \brief Creates a new SSLCert. + * + * The certificate and key data may be NULL (default values) if the certificate is meant + * to be passed to createSelfSignedCert(). + * + * Otherwise, the data must reside in a memory location that is not deleted until the server + * using the certificate is stopped. + * + * \param[in] certData The certificate data to use (DER format) + * \param[in] certLength The length of the certificate data + * \param[in] pkData The private key data to use (DER format) + * \param[in] pkLength The length of the private key + */ + SSLCert( + unsigned char * certData = NULL, + uint16_t certLength = 0, + unsigned char * pkData = NULL, + uint16_t pkLength = 0 + ); + virtual ~SSLCert(); + + /** + * \brief Returns the length of the certificate in byte + */ + uint16_t getCertLength(); + + /** + * \brief Returns the length of the private key in byte + */ + uint16_t getPKLength(); + + /** + * \brief Returns the certificate data + */ + unsigned char * getCertData(); + + /** + * \brief Returns the private key data + */ + unsigned char * getPKData(); + + /** + * \brief Sets the private key in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert() for some information on how to generate DER data. + * + * \param[in] _pkData The data of the private key + * \param[in] length The length of the private key + */ + void setPK(unsigned char * _pkData, uint16_t length); + + /** + * \brief Sets the certificate data in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert for some information on how to generate DER data. + * + * \param[in] _certData The data of the certificate + * \param[in] length The length of the certificate + */ + void setCert(unsigned char * _certData, uint16_t length); + + /** + * \brief Clears the key buffers and deletes them. + */ + void clear(); + +private: + uint16_t _certLength; + unsigned char * _certData; + uint16_t _pkLength; + unsigned char * _pkData; + +}; + +/** + * \brief Defines the key size for key generation + * + * Not available if the `HTTPS_DISABLE_SELFSIGNING` compiler flag is set + */ +enum SSLKeySize { + /** \brief RSA key with 1024 bit */ + KEYSIZE_1024 = 1024, + /** \brief RSA key with 2048 bit */ + KEYSIZE_2048 = 2048, + /** \brief RSA key with 4096 bit */ + KEYSIZE_4096 = 4096 +}; + +/** + * \brief Creates a self-signed certificate on the ESP32 + * + * This function creates a new self-signed certificate for the given hostname on the heap. + * Make sure to clear() it before you delete it. + * + * The distinguished name (dn) parameter has to follow the x509 specifications. An example + * would be: + * CN=myesp.local,O=acme,C=US + * + * The strings validFrom and validUntil have to be formatted like this: + * "20190101000000", "20300101000000" + * + * This will take some time, so you should probably write the certificate data to non-volatile + * storage when you are done. + * + * Setting the `HTTPS_DISABLE_SELFSIGNING` compiler flag will remove this function from the library + */ +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom = "20190101000000", std::string validUntil = "20300101000000"); + +#endif /* SRC_SSLCERT_HPP_ */