MQTT研究之EMQ:【CoAP协议应用开发】

时间:2021-11-15 15:24:03

本博文的重点是尝试CoAP协议的应用开发,其中包含CoAP协议中一个重要的开源工具libcoap的安装和遇到的问题调研。当然,为了很好的将EMQ的CoAP协议网关用起来,也调研了下EMQ体系下,CoAP的使用逻辑, CoAP支持明文,也支持DTLS的安全传输。

首先,介绍下libcoap的环境准备,然后基于libcoap进行EMQ的CoAP协议支持的验证。我的环境信息如下:

1. Linux: 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

2. libcoap: 4.1.1

3. EMQ:起初10.95.200.11上是EMQ v2.3.11,后来验证coap无法正常工作,将EMQ的V3版本即emqx v3.0.1在10.95.197.8上安装,再次验证coap工作。

为了验证后面的COAPS通信,即CoAP基于DTLS的安全通信,这里,将libcoap的SSL环境也做一下准备,libcoap支持多种SSL的组件,这里,选择最为基础的且是最为常用的组件openssl。

1. libcoap带安全组件的环境构建

1) 首先安装openssl, 需要的openssl的版本比较高点,操作系统原始带有的openssl版本为1.0.1,安装openssl也比较简单,就不多说,我这里用的是openssl-1.1.1b.tar.gz。

[root@tkwh-kfcs-app1 libcoap]# openssl version
OpenSSL 1.1.1b 26 Feb 2019

2) 将libcoap-4.1.1.tar.gz从官网下载后,解压然后执行配置,如下。

[root@tkwh-kfcs-app1 libcoap]# ./configure --enable-documentation=no --enable-tests=no --with-openssl
openssl: error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

遇到图中所示的错误,这个错误是说动态链接库依赖找不到,其实,这种问题,通常是构建软连接即可解决,因为openssl安装好的话,ssl的动态链接库都是会有的。将自己安装的openssl的动态链接库建立一个软连接,放在/usr/lib64目录下。

ln -s /usr/local/lib64/libssl.so.1.1 /usr/lib64/libssl.so.1.1
ln -s /usr/local/lib64/libcrypto.so.1.1 /usr/lib64/libcrypto.so.1.1

3) 再次执行libcoap的安装

[root@tkwh-kfcs-app1 libcoap]# ./configure --enable-documentation=no --enable-tests=no --with-openssl
configure: error: ==> OpenSSL 1.0.1e too old. OpenSSL >= 1.1.0 required for suitable DTLS support build

检查模块版本号:

[root@tkwh-kfcs-app1 libcoap]# pkg-config --modversion openssl
1.0.1e

查看libcoap中的configure文件发现,系统查找路径使用的pkg-configure

[root@tkwh-kfcs-app1 libcoap]# find / -name pkgconfig
/usr/lib64/pkgconfig
/usr/share/pkgconfig
/usr/local/lib64/pkgconfig
/usr/local/openssl/lib/pkgconfig

结合上面安装openssl的时间(4月21)查看,新安装的openssl对应的pkgconfig应该是/usr/local/lib64/pkgconfig或者/usr/local/openssl/lib/pkgconfig

[tkiot@tkwh-kfcs-app1 openssl-1.1.1b]$ cd /usr/local/openssl/lib/pkgconfig
[tkiot@tkwh-kfcs-app1 pkgconfig]$ ll
total
-rw-r--r--. root root Apr : libcrypto.pc
-rw-r--r--. root root Apr : libssl.pc
-rw-r--r--. root root Apr : openssl.pc
[tkiot@tkwh-kfcs-app1 pkgconfig]$ cd /usr/local/lib64/pkgconfig
[tkiot@tkwh-kfcs-app1 pkgconfig]$ ll
total
-rw-r--r--. root root Apr : libcrypto.pc
-rw-r--r--. root root Apr : libssl.pc
-rw-r--r--. root root Apr : openssl.pc
[tkiot@tkwh-kfcs-app1 pkgconfig]$

配置一下包环境变量PKG_CONFIG_PATH

[root@tkwh-kfcs-app1 libcoap]# export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib64/pkgconfig
[root@tkwh-kfcs-app1 libcoap]#
[root@tkwh-kfcs-app1 libcoap]# pkg-config -modversion openssl
Unknown option -modversion
[root@tkwh-kfcs-app1 libcoap]# pkg-config --modversion openssl
1.1.1b

4) 再次执行libcoap的配置安装环境

[root@tkwh-kfcs-app1 libcoap]# ./configure --enable-documentation=no --enable-tests=no --with-openssl
。。。。
libcoap configuration summary:
libcoap package version : "4.2.0"
libcoap library version : "1.0.1"
libcoap API version : ""
libcoap DTLS lib extn : "-openssl"
host system : "x86_64-unknown-linux-gnu"
build DTLS support : "yes"
--> OpenSSL around : "yes" (found OpenSSL 1.1.1b)
OPENSSL_CFLAGS : "-I/usr/local/include "
OPENSSL_LIBS : "-L/usr/local/lib64 -lssl -lcrypto "
build doxygen pages : "no"
build man pages : "no"
build unit test binary : "no"
build examples : "yes"
build with gcov support : "no"
[root@tkwh-kfcs-app1 libcoap]#

2. 配置emq的coap环境

其实很简单,就是一个plugin。

## Value: Port
coap.port = ## Interval for keepalive, specified in seconds.
##
## Value: Duration
## -s: seconds
## -m: minutes
## -h: hours
coap.keepalive = 120s ## Whether to enable statistics for CoAP clients.
##
## Value: on | off
coap.enable_stats = off ## Private key file for DTLS
##
## Value: File
coap.keyfile = /opt/certs/ecc/eccEmqCertPem.key ## Server certificate for DTLS.
##
## Value: File
coap.certfile = /opt/certs/ecc/eccEmqCert.crt

上述的coap的keyfile和certfile的内容,是基于上一篇博文MQTT研究之EMQ:【CoAP协议的ECC证书研究】创建出来的。

然后执行加载插件指令:

[root@tkwh-kfcs-app1 emqttd]#./bin/emqttd_ctl plugins load emq_coap

用nc指令检查端口是否活着(注意,首先想到的Telnet指令,是不行的,Telnet是检查TCP端口的

[root@tkwh-kfcs-app1 emqttd]# nc -vu 10.95.200.11 5683
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Connected to 10.95.200.11:.

到此,说明EMQ的CoAP环境构建成功,且CoAP的客户端测试环境libcoap也已经成功搭建。

3. CoAP基本的返回码信息介绍

CoAP协议是类似HTTP的风格的协议,只是底层是基于UDP和HTTP底层是基于TCP的协议。 在CoAP响应中,Code被定义为CoAP响应码,类似于HTTP 200 OK等等。

【2.01】Created
【2.02】Deleted
【2.03】Valid
【2.04】Changed
【2.05】Content。类似于HTTP OK 【4.00】Bad Request 请求错误,服务器无法处理。类似于HTTP 。
【4.01】Unauthorized 没有范围权限。类似于HTTP 。
【4.02】Bad Option 请求中包含错误选项。
【4.03】Forbidden 服务器拒绝请求。类似于HTTP 。
【4.04】Not Found 服务器找不到资源。类似于HTTP 。
【4.05】Method Not Allowed 非法请求方法。类似于HTTP 。
【4.06】Not Acceptable 请求选项和服务器生成内容选项不一致。类似于HTTP 。
【4.12】Precondition Failed 请求参数不足。类似于HTTP 。
【4.15】Unsuppor Conten-Type 请求中的媒体类型不被支持。类似于HTTP 。 【5.00】Internal Server Error 服务器内部错误。类似于HTTP 。
【5.01】Not Implemented 服务器无法支持请求内容。类似于HTTP 。
【5.02】Bad Gateway 服务器作为网关时,收到了一个错误的响应。类似于HTTP 。
【5.03】Service Unavailable 服务器过载或者维护停机。类似于HTTP 。
【5.04】Gateway Timeout 服务器作为网关时,执行请求时发生超时错误。类似于HTTP 。
【5.05】Proxying Not Supported 服务器不支持代理功能。

4. 验证CoAP协议网关工作状态

这里有点需要强调,coap插件,我在V2的版本下,验证没有通过,同样的客户端程序(Sender),同样的配置,broker采用emqx时,验证通过

发送端(java):

package com.taikang.iot.scc.research.coap;

import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.core.coap.CoAP; import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.Scanner; import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_OCTET_STREAM;
import static org.eclipse.californium.core.coap.MediaTypeRegistry.TEXT_PLAIN; /**
* @Author: chengsh05
* @Date: 2019/4/19 11:10
*/
public class CoAPSender { public static void main(String[] args) throws URISyntaxException, InterruptedException {
URI uri = new URI("coap://10.95.197.8:5683/mqtt/taikang/coapt?c=coaps1&u=water&p=water"); //创建一个资源请求taikang资源,注意默认端口为5683
CoapClient client = new CoapClient(uri);
// Scanner scan = new Scanner(System.in);
// String inputChar = scan.nextLine();
while (true) {
String payload = "hello, " + new Date().toString(); //将键盘输入的payload初始化(非CoAP)
//CoapResponse response = client.put(payload, TEXT_PLAIN);//设置PUT的内容和内容的类型TEXT_PLAIN
//client.useCONs(); CoapResponse response = client.put(payload, APPLICATION_OCTET_STREAM);//设置PUT的内容和内容的类型APPLICATION_OCTET_STREAM
// System.out.println(response.getCode());
// System.out.println(response.getOptions());
// System.out.println(response.getResponseText());
System.out.println(Utils.prettyPrint(response));
// inputChar = scan.nextLine();
Thread.sleep();
} }
}

这里,针对EMQ的CoAP协议使用,需要注意点事项:https://github.com/emqx/emqx-coap,这里有较为明确清晰的要求。

JAVA客户端日志:

::58.644 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.stack.ReliabilityLayer - Send request, failed transmissions:
::58.644 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.UdpMatcher - tracking open request [MID: , Token: [e24d0df69f8a1071]]
::58.644 [UDP-Sender-0.0.0.0/0.0.0.0:[]] DEBUG org.eclipse.californium.elements.UDPConnector - UDPConnector (Thread[UDP-Sender-0.0.0.0/0.0.0.0:[],,Californium/Elements]) sends bytes to /10.95.197.8:
::58.652 [UDP-Receiver-0.0.0.0/0.0.0.0:[]] DEBUG org.eclipse.californium.elements.UDPConnector - UDPConnector (0.0.0.0/0.0.0.0:) received bytes from /10.95.197.8:
::58.652 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.InMemoryMessageExchangeStore - removing exchange for MID KeyMID[, [0a5fc508]:]
::58.652 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.UdpMatcher - closed open request [KeyMID[, [0a5fc508]:]]
::58.652 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.InMemoryMessageExchangeStore - removing exchange for token Token[[e24d0df69f8a1071]]
::58.652 [CoapEndpoint-UDP-0.0.0.0/0.0.0.0:#] DEBUG org.eclipse.californium.core.network.UdpMatcher - Exchange [Token[[e24d0df69f8a1071]], origin: LOCAL] completed
==[ CoAP Response ]============================================
MID :
Token : [e24d0df69f8a1071]
Type : ACK
Status : 2.04
Options: {}
RTT : ms
Payload: Bytes

接收端(mosquitto工具基于mqtt协议接收,注意coap协议接收不到的,因为emqx_coap在emq里面是个协议网关,将监听到的coap协议数据转化为mqtt协议数据):

[root@ws2 ~]# mosquitto_sub -d -h 10.95.197.8 -p  -t 'taikang/v3s' -i client21 -u shihuc -P shihuc
Client client21 sending CONNECT
Client client21 received CONNACK ()
Client client21 sending SUBSCRIBE (Mid: , Topic: taikang/v3s, QoS: )
Client client21 received SUBACK
Subscribed (mid: ):
Client client21 received PUBLISH (d0, q0, r0, m0, 'taikang/v3s', ... ( bytes))
hello, Thu Jun :: CST

这里需要说明的是,基于mosquitto进行订阅操作,topic的值,和EMQ的coap协议网关下的topic对应关系,需要仔细阅读emqx_coap的官方文档要求:

CoAP Client Publish Operation
Issue a coap put command to do publishment. For example: PUT coap://localhost/mqtt/{topicname}?c={clientid}&u={username}&p={password} "mqtt" in the path is mandatory.
replace {topicname}, {clientid}, {username} and {password} with your true values.
{topicname} and {clientid} is mandatory.
if clientid is absent, a "bad_request" will be returned.
{topicname} in URI should be percent-encoded to prevent special characters, such as + and #.
{username} and {password} are optional.
if {username} and {password} are not correct, an uauthorized error will be returned.
payload could be any binary data.
payload data type is "application/octet-stream".
publish message will be sent with qos0.

注意:之前在V2.3.11的版本上操作coap协议的消息收发,遇到EMQ端总是爆出下面的错误:

当CoAP的服务端采用EMQTT,即V2版本(V2.3.11)时,服务端总是报错,提示下面的错误:
::00.538 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:06 CST 2019">>}
::05.554 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:11 CST 2019">>}
::10.564 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:16 CST 2019">>}
::15.574 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:21 CST 2019">>}

查询了很多资料(尤其针对emq_coap的插件介绍说明),都么有办法解决,百度了很多,也找不到相关信息。最后怀疑是不是V2的版本有bug,索性换了V3的EMQ服务端(emqx-centos7-v3.0.1.x86_64.rpm),客户端的java程序逻辑(URI地址IP不同,仅仅)不变,连接到V3版本一切正常。说明V2版本是有bug么?针对这个问题,我对EMQ开源项目提了一个issue(https://github.com/emqx/emqx/issues/2468),有知道问题根源的同学,也可以给我留言,这个是我什么地方搞错了么?

5. coaps的支持

在这个需求下,coap-client必须是有SSL支持的,这里是openssl组件。若没安装SSL的组件,会遇到下面的错误:

[root@mq2 libcoap]# coap-client -m put -e "hahah, coap" -c /opt/certs/ecc/ecccrtkey.crt -C /opt/certs/ecc/eccRootCert.crt "coaps://10.95.197.8:5683/mqtt/taikang/rulee?c=coap001&u=water&p=water"
Apr 25 11:11:31 EMRG coaps URI scheme not supported in this version of libcoap

这个需要libcoap的安装的时候指定DTLS的支持,若不指定,且当前安装libcoap的服务器上也没有ssl相关的环境(openssl,或者gnutls等),那么libcoap安装后是不支持coaps协议的。

执行coap-client发布coaps协议的消息:

[root@mq2 libcoap]# coap-client -m put -e "hahah, coap" -c /opt/certs/ecc/ecccrtkey.crt -C /opt/certs/ecc/eccRootCert.crt "coaps://10.95.197.8:5683/mqtt/taikang/rulee?c=coap001&u=water&p=water"
coap-client: error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

这个错误,参照前面提到的解决方案,这里的信息主要是一个填坑备忘。

上述问题都解决了,发现coap-client进行coaps的通信,还是失败(emqx的日志中显示下面的问题)。

unexpected massage {datagram,<<,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,
,,,,,,,,,,,,
,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,>>}

这是第一次尝试EMQ的coap协议网关功能,参考了EMQX的官方资料,还是解决不了上述的coaps通信问题,于是,尝试自己在本地构建coap的服务端和coap的客户端,基于java,并构建DTLS的通信环境。

然后参照org.eclipse.californium(core, connector, scandium),参照https://github.com/eclipse/californium/tree/master/demo-apps/cf-secure/src/main/java/org/eclipse/californium/examples上面的DTLS通信的例子,自己实现了一个客户端和一个服务端,在本地机器上模拟coap的通信。

客户端程序:

/*******************************************************************************
* Copyright (c) 2015 Institute for Pervasive Computing, ETH Zurich and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Matthias Kovatsch - creator and main architect
******************************************************************************/
package com.taikang.iot.scc.research.coaps; import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.logging.Level; import com.taikang.iot.scc.research.security.SSLUtils;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.ScandiumLogger;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.pskstore.StaticPskStore; import static org.eclipse.californium.core.coap.MediaTypeRegistry.APPLICATION_OCTET_STREAM; public class SecureClient { static {
ScandiumLogger.initialize();
ScandiumLogger.setLevel(Level.FINE);
} static String basePath = "E:\\2018\\IOT\\MQTT\\ssl\\"; //If you have modified GenKeys.sh modify the following variables accordingly
private final static String clientCrt = basePath + "eccDevCert.crt";
private final static String clientKey = basePath + "eccDevCert.key";
private final static String serverCrt = basePath + "eccEmqCert.crt";
private final static String serverKey = basePath + "eccEmqCert.key";
private final static String coapsCA = basePath + "eccRootCert.crt"; // private static final String SERVER_URI = "coap://10.95.197.8:5683/mqtt/taikang/rulee?c=coaps007&u=water&p=water"; private static final String SERVER_URI = "coaps://10.95.177.137:5684/mqtt"; private DTLSConnector dtlsConnector; public SecureClient() {
//Here starts DTLS configuration of the client
//load the trust store
PrivateKey cliKey = SSLUtils.loadPrivateKey(clientKey);
X509Certificate cliCrt = SSLUtils.loadCertficate(clientCrt); // load trust store
X509Certificate rootCrt = SSLUtils.loadCertficate(coapsCA); // You can load multiple certificates if needed
Certificate[] trustedCertificates = new Certificate[];
trustedCertificates[] = rootCrt; DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new InetSocketAddress());
builder.setPskStore(new StaticPskStore("Client_identity", "secretPSK".getBytes()));
builder.setIdentity(cliKey, new Certificate[]{cliCrt}, true);
builder.setTrustStore(trustedCertificates);
dtlsConnector = new DTLSConnector(builder.build());
} public void test() { CoapResponse response = null;
try {
URI uri = new URI(SERVER_URI);
CoapClient client = new CoapClient(uri);
client.setEndpoint(new CoapEndpoint(dtlsConnector, NetworkConfig.getStandard()));
while (true) {
String payload = "hello, " + new Date().toString(); //将键盘输入的payload初始化(非CoAP)
response = client.put(payload, APPLICATION_OCTET_STREAM);//设置PUT的内容和内容的类型APPLICATION_OCTET_STREAM
if(response != null) {
System.out.println(Utils.prettyPrint(response));
}else {
System.out.println("there is no response for this put operation");
} // response = client.get();
// System.out.println(Utils.prettyPrint(response));
Thread.sleep();
} } catch (URISyntaxException e) {
System.err.println("Invalid URI: " + e.getMessage());
System.exit(-);
} catch (InterruptedException e) {
e.printStackTrace();
} if (response != null) {
//
// System.out.println(response.getCode());
// System.out.println(response.getOptions());
// System.out.println(response.getResponseText()); // System.out.println("\nADVANCED\n");
System.out.println(Utils.prettyPrint(response)); } else {
System.out.println("No response received.");
}
} public static void main(String[] args) throws InterruptedException { SecureClient client = new SecureClient();
client.test(); synchronized (SecureClient.class) {
SecureClient.class.wait();
}
}
}

服务端程序:

/*******************************************************************************
* Copyright (c) 2015 Institute for Pervasive Computing, ETH Zurich and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Matthias Kovatsch - creator and main architect
******************************************************************************/
package com.taikang.iot.scc.research.coaps; import java.net.InetSocketAddress;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.logging.Level; import com.taikang.iot.scc.research.security.SSLUtils;
import org.eclipse.californium.core.CaliforniumLogger;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.coap.CoAP.ResponseCode;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.Endpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.core.network.interceptors.MessageTracer;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.ScandiumLogger;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; public class SecureServer { static {
CaliforniumLogger.initialize();
CaliforniumLogger.setLevel(Level.CONFIG);
ScandiumLogger.initialize();
ScandiumLogger.setLevel(Level.FINER);
} // allows configuration via Californium.properties
public static final int DTLS_PORT = NetworkConfig.getStandard().getInt(NetworkConfig.Keys.COAP_SECURE_PORT); static String basePath = "E:\\2018\\IOT\\MQTT\\ssl\\"; //If you have modified GenKeys.sh modify the following variables accordingly
private final static String clientCrt = basePath + "eccDevCert.crt";
private final static String clientKey = basePath + "eccDevCert.key";
private final static String serverCrt = basePath + "eccEmqCert.crt";
private final static String serverKey = basePath + "eccEmqCert.key";
private final static String coapsCA = basePath + "eccRootCert.crt"; public static void main(String[] args) { CoapServer server = new CoapServer();
server.add(new CoapResource("hello") {
@Override
public void handleGET(CoapExchange exchange) {
exchange.respond(ResponseCode.CONTENT, "handleGET==>hello," + new Date().toString());
}
}); server.add(new CoapResource("mqtt") {
@Override
public void handlePUT(CoapExchange exchange) {
exchange.respond(ResponseCode.CONTENT, "handlePUT==>mqtt," + new Date().toString());
}
}); server.add(new CoapResource("coap") {
@Override
public void handlePOST(CoapExchange exchange) {
exchange.respond(ResponseCode.CONTENT, "handlePOST==>coap," + new Date().toString());
}
}); // Pre-shared secrets
//Here starts DTLS configuration of the client
//load the trust store
PrivateKey svrKey = SSLUtils.loadPrivateDERKey(serverKey);
X509Certificate svrCrt = SSLUtils.loadCertficate(serverCrt); // load trust store
X509Certificate rootCrt = SSLUtils.loadCertficate(coapsCA); // You can load multiple certificates if needed
Certificate[] trustedCertificates = new Certificate[];
trustedCertificates[] = rootCrt; DtlsConnectorConfig.Builder config = new DtlsConnectorConfig.Builder(new InetSocketAddress(DTLS_PORT));
config.setSupportedCipherSuites(new CipherSuite[]{
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8});
config.setIdentity(svrKey, new Certificate[]{svrCrt}, true);
config.setTrustStore(trustedCertificates);
//默认情况下,服务端是要对客户端的安全要做验证的(即所谓的双向验证)
config.setClientAuthenticationRequired(false); DTLSConnector connector = new DTLSConnector(config.build()); server.addEndpoint(new CoapEndpoint(connector, NetworkConfig.getStandard()));
server.start(); // add special interceptor for message traces
for (Endpoint ep : server.getEndpoints()) {
ep.addInterceptor(new MessageTracer());
} System.out.println("Secure CoAP server powered by Scandium (Sc) is listening on port " + DTLS_PORT);
} }

PS:

1. 用java的secureServer作为server,然后用coap-client作为client进行测试,验证逻辑, coap-client的日志如下:

[root@mq2 ecc]# coap-client -t binary -c /opt/certs/ecc/ecccrtkey.crt -C /opt/certs/ecc/eccRootCert.crt -m get "coaps://10.95.177.137/hello"
Apr :: WARN 10.95.197.8: <-> 10.95.177.137: DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=
handleGET==>hello,Sun Apr :: CST
[root@mq2 ecc]#
[root@mq2 ecc]# coap-client -t binary -c /opt/certs/ecc/ecccrtkey.crt -C /opt/certs/ecc/eccRootCert.crt -e "hello mqtt" -m put "coaps://10.95.177.137/mqtt"
Apr :: WARN 10.95.197.8: <-> 10.95.177.137: DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=
handlePUT==>mqtt,Sun Apr :: CST
[root@mq2 ecc]#
[root@mq2 ecc]# coap-client -t binary -c /opt/certs/ecc/ecccrtkey.crt -C /opt/certs/ecc/eccRootCert.crt -e "hello mqtt with coaps" -m post "coaps://10.95.177.137/coap"
Apr :: WARN 10.95.197.8: <-> 10.95.177.137: DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=
handlePOST==>coap,Sun Apr :: CST

注意:服务端的COAPResource的path部分填写,即上面代码中的hello, mqtt, coap, 这些针对不同的method(hello:get, mqtt:put, coap:post),不能出现同一个path,对应不同的method,就类似同一个主题或者URL,即有发布又有订阅,是不允许的,程序执行时,client端会报4.05的错误.

2.coap-client的指令使用说明中关于coaps配置(COAP协议,为了保证传输信息量尽量下,证书算法支持ECC和PSK两种,其他的似乎不支持【至少在california的工具包里面是限制了】),有下面的说明:

PSK Options (if supported by underlying (D)TLS library)
-k key Pre-shared key for the specified user
-u user User identity for pre-shared key mode
PKI Options (if supported by underlying (D)TLS library)
-c certfile PEM file containing both CERTIFICATE and PRIVATE KEY
This argument requires (D)TLS with PKI to be available
-C cafile PEM file containing the CA Certificate that was used to
sign the certfile. This will trigger the validation of
the server certificate. If certfile is self-signed (as
defined by '-c certfile'), then you need to have on the
command line the same filename for both the certfile and
cafile (as in '-c certfile -C certfile') to trigger
validation
-R root_cafile PEM file containing the set of trusted root CAs that
are to be used to validate the server certificate.
The '-C cafile' does not have to be in this list and is
'trusted' for the verification.
Alternatively, this can point to a directory containing
a set of CA PEM files

上述的我的案例中,用的是PKI的模式,采用ECC的证书。参数-c certfile说明要求,证书和私钥要在一个配置文件中,我这里将私钥append到证书的后面了,本案例中,ecccrtkey.crt文件如下:

-----BEGIN CERTIFICATE-----
MIICDzCCAbOgAwIBAgIEXMAt2jAMBggqhkjOPQQDAgUAMGYxEzARBgNVBAMMCklPVF9FQ0NfQ0Ex
CzAJBgNVBAYTAkNOMQ4wDAYDVQQIEwVIdWJlaTEOMAwGA1UEBxMFV3VoYW4xEDAOBgNVBAoTB1RL
Q2xvdWQxEDAOBgNVBAsTB1RhaUthbmcwHhcNMTkwNDI0MDkzNTIyWhcNMjAwNDIzMDkzNTIyWjBm
MRMwEQYDVQQDDApJT1RfRGV2aWNlMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFSHViZWkxDjAMBgNV
BAcTBVd1aGFuMRAwDgYDVQQKEwdUS0Nsb3VkMRAwDgYDVQQLEwdUYWlLYW5nMFkwEwYHKoZIzj0C
AQYIKoZIzj0DAQcDQgAEcQVnG7L5k0YqSYnw+DFc4FjFfdKsBK28AYQ4uOnzzHxHRQNgJZqMHFYO
abMWpmgUjhg2akpHf5xQOPEiLGXl/aNNMEswHwYDVR0jBBgwFoAUp1pH8oPTujZTqsR5cPYf0m3T
DxQwCQYDVR0TBAIwADAdBgNVHQ4EFgQU3I9TpzP9ohiYyqy15fdSBlSLdrAwDAYIKoZIzj0EAwIF
AANIADBFAiEAlrtKf38SF05Pm48GMirVVnqkUli/YDRE51+SHVvgSq0CIBPrrIw4/51XRpC19ml6
iPwF4adyy5+QTU1cSVXmv6KS
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDnfo/KSIXSc9/8CR8B
zEjgIpem2rty55ReGShwUGp0sg==
-----END PRIVATE KEY-----

这里的-C cafile的描述,指的就是签发证书,我这里就是自签名证书。指令中么有使用-R这个选项。

JAVA的模拟COAPS的通信是正常的,没有任何问题。但是在emqx的环境下验证coaps,验证通信是失败的,初步得出结论:
1. EC椭圆曲线算法生成的证书是没有问题的。
2. COAPS的通信逻辑是基本走通了,没有问题的。
3. EMQX的coaps通信逻辑目前应该是有问题的,或者是基本没有做支持。

针对这个EMQX对COAPS的支持,我也向EMQ团队提出了ISSUE(https://github.com/emqx/emqx-docs-cn/issues/136)这个,可以说明EMQ目前在快速迭代,里面也存在一些问题,需要开发者和应用者及时验证和反馈,不然,应用很可能会出现问题,自己都无法知道根源。

好了,到此,有什么需要探讨的,可以关注我的博客,一起交流。