在微控制器和物联网上使用JavaScript:SSL / TLS

在最新的《在微控制器和物联网上使用JavaScript》的文章中,我们发现Espruino在Esp8266平台上非常受欢迎,Espruino的确很不错,但在EPS8266平台上还是非常有限的。Espruino提供了TLS支持,这一功能在其他硬件平台非常有用。在今天的这篇文章中,我们回到Particle Photon上来解决他的一个最大的缺点:缺少TLS支持,接下来我们将详细介绍如何添加这一功能。

在整个《在微控制器和物联网上使用JavaScript》系列中,我们探索了在微控制器平台上添JavaScript的各种方法。我们还学习了如何使用C和JavaScript库。然而,到目前为止,我们还是遗漏了一个问题,那就是安全通信。

当我们研究Particle Photon作为替代方案时,其固件为我们提供了一种安全通信的方法:即与Particle Cloud相连。Particle Cloud是由Photon的开发者Particle提供的一系列在线服务。它非常的方便:易于使用,加密,并且能够充当通往其他在线服务的网关。Particle Cloud允许我们将我们的传感器数据安全地发送到WebTask。这很好,但是,对于某些应用程序是无法依靠外部云平台的。另外,粒子云有其自身的局限性,可能不足以达到我们的目的。我们需要一个替代品。

另一方面,我们也在ESP8266上看过Espruino。Espruino提供了对TLS的支持,但是,它仅适用于某些特定的硬件。至于ESP8266,它默认是禁用的。因此,Espruino和ESP8266不是安全通信的有效替代方案。

在这篇文章中,我们将思考一个更加显而易见的方案:为Particle Photon添加一个安全的通信库。对此有很多选择,但我们会选择互联网上使用最多的:TLS。这将允许我们在没有网关或代理服务器(如中间的Particle Cloud)的情况下与常用服务进行通信。

小型TLS库

TLS及其前身SSL都非常大。它支持许多密码和算法。因此,为了将代码大小和内存使用量降到最低,非常重要的一点就是我们需要选择一个专为此设计的库。该领域中有三个主要竞争者:Mbed TLS(以前称为PolarSSL)wolfSSL(以前称为CyaSSL)GUARD TLS(以前称为MatrixSSL)。他们三个都是开源的,且拥有免费软件许可证,但其中只有Mbed TLS允许其在闭源项目中免费使用。这三者在嵌入式社区中都很受欢迎,所以选择哪一个取决于您的用例或许可要求,为了保持我们所有的选择公开,我们选择了Mbed TLS。

TLS概述

传输层安全性(TLS)是计算机网络上安全通信的标准。它的前身安全套接字层(SSL)由Netscape于1994年为其Web浏览器开发.TLS用于Internet上的安全通信,并且是由HTTPS实现的安全层。HTTPS基本上就是HTTP,但启用了TLS的TCP / IP Socket(这意味着几乎所有的互联网事务都依赖于TLS来实现安全性)。TLS支持许多不同的算法,提供各种级别的安全性和计算复杂性。它还支持高级功能,如前向保密,即使未来密钥泄漏,也可防止解密过去的消息。

TLS通过非对称和对称密码的组合工作。非对称密码学使用两个密钥,一个公共密钥和一个私密密钥,以允许加密数据在单一方向上流动。公钥用来加密只有私钥可以解密的数据。另一方面,对称密码使用单个共享密钥来加密和解密数据。这意味着任何知道共享密钥的人都可以读取和更改使用它加密的数据。

TLS的工作方式是在公共证书的形式下在通信双方中的一个保留一系列可信的公钥。证书是关于由该实体(或其他人)使用其私钥签署的实体信息。证书内也携带实体的公钥,实体的公钥允许任何人检查由该实体执行签名的有效性。换句话说,如果你有一个实体的证书,你就知道实体的数据(比如它的名字),你可以用它的公钥执行两个动作:为它加密数据,或者检查它对数据签名的有效性。但要真正确定这意味着什么,您需要确保公钥来自正确的实体,并且相信它。

在启动连接之前,一组可信任的证书必须事先存在于支持TLS的客户端中。至于网络环境中,Web浏览器和操作系统在安装时会附带自己的一组可信证书。

当客户端启动连接时,客户端连接到服务器并请求服务器发送其公共证书以及为了验证证书的合法性所需的其他证书。这是在没有任何加密的情况下在公开场合完成的。然后,客户端可以通过从其可信证书之一(预先安装在客户端中的证书)中查找签名来验证服务器的证书。服务器通常依靠中间证书进行验证,这意味着这些中间证书可能由不是来自可信组的证书进行签名,而我们必须用一个可信证书对中间证书进行签名。因此,为了检查服务器证书的有效性,客户端要沿着证书签名链直到找到一个可信证书的签名。如果所有签名都通过检验,那么收到的证书可以被认为是有效的。对于互联网连接,一旦服务器的证书得到验证,客户端必须将其中的一个字段(公用名称(CN))与启动连接时请求的主机名进行比较。因此,如果客户想连接www.google.com,证书必须为www.google.com(证书中的通用名称必须与浏览器请求的地址相匹配),这取决于控制一组可信任证书的实体,以确保为特定域请求证书的人是该域的实际所有者。这样,只有Google(公司)才能为www.google.com申请证书。

一旦证书被验证并且域名与通用名称匹配,就可以建立一个安全的通信通道。为此,TLS使用密钥交换算法。这些算法依赖于服务器的证书和非对称加密来协商服务器和客户端之间的新共享密钥。TLS支持多种不同的算法。一旦这个密钥建立,通信就切换到对称加密。对称加密比非对称加密更有效,因此更适合于在初始握手后与服务器交换数据。TLS也支持不同的对称算法,但大多数情况下选择AES的一种变体。

Mbed TLS

Mbed TLS是一个C库。它需要一个C99编译器并且高度可配置。我们将删除任何不必要的内容,如文件访问,旧版本支持(SSLv3),Berkeley / BSD / Linux套接字等,以将代码大小降至最低。

警告

在继续之前需要谨慎小心。TLS所需的一些算法依赖于一个好的熵源,熵源是随机性的来源,通常用于提供Mbed TLS内部使用的随机数生成器(RNG)。这带来了一个问题:开箱即用的Particle Photon没有好的熵源。请注意,一个伪随机数发生器(PRNG)(比如Particle Firmware提供的那个)不够好。

一种选择是构建自己的,另一种选择是购买一个。无论您选择什么,请注意,没有良好的熵源可能会导致通信受损。对于我们的示例,我们已配置Mbed TLS以使用由粒子固件提供的PRNG。这不是一个好办法,不要在生产中使用它。粒子的PRNG依赖于在启动过程中设置一个真正随机的种子,除非连接到粒子云,否则不会完成。即使那样,也不能保证PRNG是一个用于加密的好熵源。如果您想在生产中使用类似的东西,请购买专业的随机数字生成器并将其集成到您的项目中。

举个例子

对于我们的例子,我们将再次转向我们的传感器集线器示例。但是,我们将使用第三篇文章中的版本并在此基础上进行构建。我们第三篇文章中的传感器集线器示例执行以下操作:

  • 它不断监视每个传感器寻找关键的条件。如果检测到严重情况,它会向“粒子云”发送警报事件。
  • 它定期向本地服务器发送传感器当前值的报告。

对于这篇文章,我们将更改示例以执行以下操作:

  • 它将持续监控每个传感器寻找关键条件。如果检测到严重情况,它将向Web任务发送HTTP请求。
  • 无论使用HTTP请求的关键条件如何,它都会周期性地向同一Web任务发送报告。

Web任务需要TLS,所以所有的HTTP请求都会被加密。

您可能已经注意到,在我们的新示例中没有提及粒子云。没错,拥有TLS让我们可以不用依靠粒子云就能进行安全通信。

第1步:设置Mbed TLS

要使用Mbed TLS,我们需要在它的网站上下载最新的压缩包,并在我们所选择的目录解压它。然后,我们调整Docker命令行以将此目录公开给Docker镜像,该镜像用于编译Particle Photon固件。

Mbed TLS支持makefile和CMake。由于我们已经在JerryScript上使用CMake,所以我们可以直接用CMake安装Mbed TLS。编译MBed TLS只是一个正确调用CMake方法的问题。

摘自Makefile.particle

mbed:
    mkdir -p $(BUILD_DIR)/mbedtls
    CFLAGS="$(EXT_CFLAGS) -Os" cmake -B$(BUILD_DIR_MBED) -H../mbedtls \\
     -DENABLE_TESTING=OFF \\
     -DUSE_SHARED_MBEDTLS_LIBRARY=OFF \\
     -DUSE_STATIC_MBEDTLS_LIBRARY=ON \\
     -DENABLE_PROGRAMS=OFF \\
     -DCMAKE_TOOLCHAIN_FILE=$(JERRYDIR)/cmake/toolchain_external.cmake \\
     -DEXTERNAL_CMAKE_SYSTEM_PROCESSOR=armv7l \\
     -DEXTERNAL_CMAKE_C_COMPILER=arm-none-eabi-gcc \\
     -DEXTERNAL_CMAKE_C_COMPILER_ID=GNU
    make VERBOSE=1 -C$(BUILD_DIR_MBED)

我们希望构建精简而干净的Mbed TLS版本而不需要任何多余的功能,例如文件访问。所有Mbed TLS选项均通过其配置文件config.h进行设置。来看我们为这个例子选择的设置。支持TLS 1.1,RSA,椭圆曲线和AES,将RAM使用量降到最低的三个重要设置是:

#define MBEDTLS_MPI_WINDOW_SIZE 1
#define MBEDTLS_AES_ROM_TABLES
#define MBEDTLS_SSL_MAX_CONTENT_LEN 6144

第一行更改Mbed TLS使用的大数库的内部设置,这会减慢操作速度,但消耗更少的RAM。第二行告诉系统预编译AES算法使用的表并将它们存储在静态常量C数组中。这允许表格保存在ROM而不是RAM中。第三行减少了Mbed TLS使用的接收缓冲区的大小。TLS要求至少16KiB的缓冲区,但是当服务器和客户端都支持扩展或者数据永远不会超过缓冲区大小时,可以使用较小的缓冲区。

Mbed TLS的原始config.h文件已完整记录。如果您有兴趣将库调整为您的需要,请查看它

第2步:添加TLS支持到粒子的TCPClient

关于Mbed TLS的一个很酷的事情是,使用任何通信通道都非常简单。该库只需要定义两个函数:一个用于将数据写入通道,另一个用于接收数据。当然,如果您使用的是众所周知的套接字库如Berkeley Socket(大多数Unix,如macOS或Linux)或WinSock(Windows套接字,aspx),Mbed TLS为您提供了必要的功能。由于我们使用的是Particle Photon技术,我们的TCP客户端库没有开箱即用的支持。幸运的是,编写发送/接收函数非常简单:

TCPClient client;
static int
tcp_client_send(void *ctx, const unsigned char *buf, size_t len) {
    TCPClient *client = reinterpret_cast<TCPClient *>(ctx);
    return client->write(buf, len);
}
static int
tcp_client_recv(void *ctx, unsigned char *buf, size_t len) {
    TCPClient *client = reinterpret_cast<TCPClient *>(ctx);
    const int read = client->read(buf, len);
    if(read <= 0) {
        return MBEDTLS_ERR_SSL_WANT_READ;
    } else {
        return read;
    }
}
bool connect(const char *host, uint16_t port) {   
    // (...)
    mbedtls_ssl_set_bio(&ssl, &client, 
        tcp_client_send, tcp_client_recv, NULL);   
    // (...)    
}

我们编写了一个使用TCP客户端(来源于Particle)和Mbed TLS的小型C ++类来连接服务器:

struct tls_tcp_client {
    tls_tcp_client() {
        mbedtls_ssl_init(&this->ssl);
        mbedtls_ssl_config_init(&this->conf);
        mbedtls_entropy_init(&this->entropy);
        mbedtls_ctr_drbg_init(&this->ctr_drbg);
        /* This is the best entropy source we have, NOT SECURE */
        mbedtls_entropy_add_source(&entropy, get_random, NULL, 1,   
            MBEDTLS_ENTROPY_SOURCE_STRONG);
        mbedtls_ctr_drbg_seed(&ctr_drbg,
            mbedtls_entropy_func, &entropy, NULL, 0);
        mbedtls_ssl_config_defaults(&conf,
            MBEDTLS_SSL_IS_CLIENT,
            MBEDTLS_SSL_TRANSPORT_STREAM,
            MBEDTLS_SSL_PRESET_DEFAULT);
        mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
        mbedtls_ssl_conf_ca_chain(&conf, &global_tls_ca, NULL);
        mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED);
        //mbedtls_ssl_conf_verify(&conf, verify, NULL);
        mbedtls_ssl_conf_max_version(&conf, 
            MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_2);
        mbedtls_ssl_conf_min_version(&conf,
            MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_2);
    }
    bool connect(const char *host, uint16_t port) {     
        mbedtls_ssl_session_reset(&ssl);
        mbedtls_ssl_setup(&ssl, &conf);        
        mbedtls_ssl_set_bio(&ssl, &client, 
            tcp_client_send, tcp_client_recv, NULL);   
        Log.trace("TLS connect to host: %s", host);
        mbedtls_ssl_set_hostname(&ssl, host);
        client.connect(host, port);
        Log.print(client.remoteIP().toString());
        int code = 0;
        unsigned long now = millis();
        while((code = mbedtls_ssl_handshake(&ssl)) ==
              MBEDTLS_ERR_SSL_WANT_READ) {
            if((millis() - now) > 10000) {
                // timeout
                break;
            }
            //Log.trace("10 Free mem: %u", System.freeMemory());
            Particle.process();
        }
        if(code != 0) {
            char buf[128];
            mbedtls_strerror(code, buf, sizeof(buf));
            Log.trace("TLS connected failed, code %i -> %s", code, buf);
            client.stop();
            return false;
        }
        return true;
    }
    // (...)
}

查看该类的所有代码

第3步:向JavaScript公开已经启用TLS的TCP客户端

由于支持TLS,公开我们最新版本的photon.TCP客户端和Javascript对象也非常简单。我们只需要使用JerryScript的API 来包装我们tls_tcp_client类。这类似于我们为普通TCP客户端所做的工作(TCP客户端由Particle固件提供)

static jerry_value_t 
create_tls_tcp_client(const jerry_value_t func,
                      const jerry_value_t thiz,
                      const jerry_value_t *args,
                      const jerry_length_t argscount) {
    jerry_value_t constructed = thiz;
    // Construct object if new was not used to call this function
    {
        const jerry_value_t ownname = create_string("TLSTCPClient");
        if(jerry_has_property(constructed, ownname)) {
            constructed = jerry_create_object();
        }
        jerry_release_value(ownname);
    }
    // Backing object
    tls_tcp_client *client = new tls_tcp_client;
    static const struct {
        const char* name;
        jerry_external_handler_t handler;
    } funcs[] = {
        { "connected", tls_tcp_client_connected },
        { "connect"  , tls_tcp_client_connect   },
        { "write"    , tls_tcp_client_write     },
        { "available", tls_tcp_client_available },
        { "read"     , tls_tcp_client_read      },
        { "stop"     , tls_tcp_client_stop      }
    };
    for(const auto& f: funcs) {
        const jerry_value_t name = create_string(f.name);
        const jerry_value_t func = jerry_create_external_function(f.handler);
        jerry_set_property(constructed, name, func);
        jerry_release_value(func);
        jerry_release_value(name);
    }
    jerry_set_object_native_pointer(constructed, client, 
        &tls_client_native_info);
    return constructed;
}

在这里,我们创建一个新的tls_tcp_client对象并将其设置为备用对象,以作为photon.TLSTCPClient对象的新实例。要查看其它的封装函数,请参阅tls_tcp_client.h文件。

我们还公开了一个允许我们全局添加新的可信证书的函数。TLS客户端必须事先知道这些证书,只有这样才能用于验证服务器发送的证书。

static jerry_value_t 
tls_tcp_client_add_certificates(const jerry_value_t func,
                                const jerry_value_t thiz,
                                const jerry_value_t *args,
                                const jerry_length_t argscount) {
    if(argscount != 1 || !jerry_value_is_string(*args)) {
        return jerry_create_error(JERRY_ERROR_TYPE, 
            reinterpret_cast<const jerry_char_t *>(
                "Expected certificate as string"));
    }
    const size_t size = jerry_get_string_size(*args);
    std::vector<char> buf(size + 1);
    jerry_string_to_char_buffer(*args, 
        reinterpret_cast<jerry_char_t*>(buf.data()), buf.size());
    int code = 0;
    if((code = mbedtls_x509_crt_parse(&global_tls_ca,
        reinterpret_cast<unsigned char *>(buf.data()),
        buf.size())) != 0) {
        Log.trace("Failed to parse certificate: %i", code);
        Log.trace("%s", buf.data());
        return jerry_create_error(JERRY_ERROR_TYPE, 
            reinterpret_cast<const jerry_char_t *>(
                "Failed to parse certificate"));
    }
    return jerry_create_undefined();
}

第4步:更改JavaScript代码

调整我们的JavaScript传感器集线器代码非常简单。我们只需要使用photon.TLSTCPClient来替代photon.TCPClient,连接到服务器之前,请确保我们已经设置了正确的可信证书。另外我们还需要更新我们用来发送数据的URL。

function buildHttpRequest(data) {
    const request = 
        `POST ${path} HTTP/1.1\\r\\n` + 
        `Host: ${host}\\r\\n` + 
        `Content-Length: ${data.length}\\r\\n` +
        `Content-Type: application/x-www-form-urlencoded\\r\\n` +
        `Secret: ${secret}\\r\\n` +
        'Connection: close\\r\\n\\r\\n' + 
        data;
    return request;
}
function sendData(data) {
    const client = photon.TLSTCPClient();
    client.connect(host, port);
    if(!client.connected()) {
        photon.log.error(`Could not connect to ${host}:${port}, ` + 
                         `discarding data.`);
        return;
    }
    client.write(buildHttpRequest(data));
    client.stop();
}

WebTask是为原始传感器集线器示例准备的,由于我们要复用它(即我们根本不会对WebTask进行任何更改),我们需要保持粒子云的格式发送数据,这非常简单。

这些数据是作为POSTHTTP请求发送的,它携带一个名为Secret特殊标题的头文件,并包含服务器必须验证的明文密码。数据是作为POST请求主体的一部分来发送,并使用URL编码,关键字如下:

  • coreid:携带 Particle Photon所发送数据的Id的字符串。
  • event:发送事件的字符串,无论是sensor-datasensor-event。
  • data:包含传感器数据的字符串。这是一个字符串化的JSON对象。
function objectUrlEncode(obj) {
    var str = [];
    for(let p in obj) {
        if(obj.hasOwnProperty(p)) {
            const key = encodeURIComponent(p);
            const val = encodeURIComponent(obj[p]);
            str.push(key + "=" + val);
        }
    }
    return str.join("&");
}
function sendEvent(event, data) {        
    try {
        const datastr = JSON.stringify(data);
        photon.log.trace(`Sending event ${event}, data: ${datastr}`);
        try {
            // Send event to our server
            sendData(objectUrlEncode({ 
                event: event,
                data: datastr,
                coreid: coreid
            }));
        } catch(e) {
            photon.log.error(`Could not send event to server: ${e.toString()}`);
        }
        // Send event to Particle cloud: disabled to reduce memory usage
        // photon.publish(event, datastr);
    } catch(e) {
        photon.log.error(`Could not publish event: ${e.toString()}`);
    }
}

该代码位于示例目录的js子文件夹中。您将需要为webtask更新Secret,coreid和URL。当所有的工作都到位,我们可以测试新的支持TLS的传感器集线器。

第5步:试试看!

我们将报告给WebTask,它与前面发布文章使用的WebTask相同,所以不需要重新部署我们的WebTask。如果您想了解如何部署WebTask,请查看第二篇文章。要在Linux上刷新Particle Photon,请将Photon置于DFU模式,然后运行以下脚本:

./compile-and-flash.sh

如果您在不同的平台上,请查看第一篇文章,了解Particle Photon的不同刷机方式。

获取完整的示例。

结论

我们已经将Particle Photon推到了极限。我们集成了一个JavaScript解释器和一个TLS库,并让他们完美运行。我们装满了所有可用的ROM和RAM,幸运的是,它工作正常!但是,我们不能推荐走这条生产线。我们不得不使用微调内存来确保一切正常。要么选择更大的微控制器,要么放弃一个元素:JavaScript或TLS。我们认为对于Particle开发人员来说,公开嵌入在固件中的Mbed TLS库是一个好主意,以便用户应用程序可以与其链接。在内存有限的设备中有两个相同的库副本是在浪费。

我们也非常有兴趣看到Espruino如何在经过验证的硬件上使用TLS,但不幸的是,现在我们没有任何权力。如果你选择在Particle Photon上使用Mbed TLS,不要忘记获得一个硬件随机数生成器,不要一开始就违背使用TLS的目的!正如我们所看到的,一旦TLS可用,微控制器变得更加强大,并且诸如WebTasks之类的大量现有服务立即可用。

到此为止,我们已经结束了针对微控制器和IoT系列的JavaScript。如果您希望我们探索与物联网相关的其他内容,请在评论中告知我们。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
在微控制器和物联网上使用JavaScript:SSL / TLS
在今天的这篇文章中,我们回到Particle Photon上来解决他的一个最大的缺点:缺少TLS支持,接下来我们将详细介绍如何添加这一功能。
<<上一篇
下一篇>>