SHIHUC

好记性不如烂笔头,还可以分享给别人看看! 专注基础算法,互联网架构,人工智能领域的技术实现和应用。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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

Posted on 2019-06-06 11:03  shihuc  阅读(4060)  评论(3编辑  收藏  举报

本博文的重点是尝试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 12
-rw-r--r--. 1 root root 299 Apr 21 09:20 libcrypto.pc
-rw-r--r--. 1 root root 278 Apr 21 09:20 libssl.pc
-rw-r--r--. 1 root root 232 Apr 21 09:20 openssl.pc
[tkiot@tkwh-kfcs-app1 pkgconfig]$ cd /usr/local/lib64/pkgconfig
[tkiot@tkwh-kfcs-app1 pkgconfig]$ ll
total 12
-rw-r--r--. 1 root root 293 Apr 21 09:26 libcrypto.pc
-rw-r--r--. 1 root root 272 Apr 21 09:26 libssl.pc
-rw-r--r--. 1 root root 226 Apr 21 09:26 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     : "2"
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 = 5683

## 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:5683.

到此,说明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 200 OK

【4.00】Bad Request 请求错误,服务器无法处理。类似于HTTP 400。
【4.01】Unauthorized 没有范围权限。类似于HTTP 401。
【4.02】Bad Option 请求中包含错误选项。
【4.03】Forbidden 服务器拒绝请求。类似于HTTP 403。
【4.04】Not Found 服务器找不到资源。类似于HTTP 404。
【4.05】Method Not Allowed 非法请求方法。类似于HTTP 405。
【4.06】Not Acceptable 请求选项和服务器生成内容选项不一致。类似于HTTP 406。
【4.12】Precondition Failed 请求参数不足。类似于HTTP 412。
【4.15】Unsuppor Conten-Type 请求中的媒体类型不被支持。类似于HTTP 415。

【5.00】Internal Server Error 服务器内部错误。类似于HTTP 500。
【5.01】Not Implemented 服务器无法支持请求内容。类似于HTTP 501。
【5.02】Bad Gateway 服务器作为网关时,收到了一个错误的响应。类似于HTTP 502。
【5.03】Service Unavailable 服务器过载或者维护停机。类似于HTTP 503。
【5.04】Gateway Timeout 服务器作为网关时,执行请求时发生超时错误。类似于HTTP 504。
【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(5000);
        }

    }
}

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

 

JAVA客户端日志:

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

 

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

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

这里需要说明的是,基于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)时,服务端总是报错,提示下面的错误:
11:03:00.538 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,60,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:06 CST 2019">>}
11:03:05.554 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,60,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:11 CST 2019">>}
11:03:10.564 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,60,<<"application/octet-stream">>,[],<<"helloSun Apr 21 11:02:16 CST 2019">>}
11:03:15.574 [error] CoAP-RES: put has error, Prefix=[<<"mqtt">>], Name=[<<"taikang">>,<<"coapt">>], Content={coap_content,undefined,60,<<"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,<<22,254,253,0,0,0,0,0,0,0,0,0,94,1,0,0,82,0,0,0,
                               0,0,0,0,82,254,253,92,193,89,246,123,189,1,34,
                               211,75,88,121,216,180,234,146,199,185,254,252,
                               84,234,1,17,156,163,161,148,128,226,178,127,0,
                               0,0,8,192,174,192,35,192,168,0,174,1,0,0,32,0,
                               10,0,8,0,6,0,23,0,24,0,25,0,11,0,2,1,0,0,19,0,
                               3,2,2,0,0,20,0,3,2,0,2>>}

 

这是第一次尝试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[1];
        trustedCertificates[0] = rootCrt;

        DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new InetSocketAddress(0));
        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(5000);
            }

        } catch (URISyntaxException e) {
            System.err.println("Invalid URI: " + e.getMessage());
            System.exit(-1);
        } 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[1];
        trustedCertificates[0] = 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 28 12:24:47 WARN     10.95.197.8:33118 <-> 10.95.177.137:5684 DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=0
handleGET==>hello,Sun Apr 28 12:28:41 CST 2019
[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 28 12:25:10 WARN     10.95.197.8:38407 <-> 10.95.177.137:5684 DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=0
handlePUT==>mqtt,Sun Apr 28 12:29:05 CST 2019
[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 28 12:25:33 WARN     10.95.197.8:43010 <-> 10.95.177.137:5684 DTLS: unable to get certificate CRL: overridden: '10.95.197.8' depth=0
handlePOST==>coap,Sun Apr 28 12:29:27 CST 2019

注意:服务端的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目前在快速迭代,里面也存在一些问题,需要开发者和应用者及时验证和反馈,不然,应用很可能会出现问题,自己都无法知道根源。

 

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