Paho-MQTT C使用ID²-SE接入阿里云物联网平台
本文介绍了开源Paho MQTT embeded-c如何通过更安全的身份认证方式(ID²)连接到阿里云物联网平台,并建立安全传输通道。
1. 术语
- Soft-KM(Key Management)密钥管理,由阿里提供的软件安全沙箱,基于软件加固和虚拟化技术提供对密钥(IoT ID²)的安全保护。 
- OSA(Operation System Abstractor Layer)操作系统抽象层,定义内存申请和释放、日志打印、系统时间、网络通信等接口,非遵循POSIX标准的OS,需重新适配这些接口。 
- HAL(Hardware Abstractor Layer)硬件抽象层,根据设备硬件特性,完成对加解密算法、设备唯一ID、密钥管理和数据安全存储的接口适配。 
- TLS(Transport Layer Security)安全传输层协议, 用于两个通信实体之间,保护通信数据的私密性和完整性。 
- IoT(Internet of things)物联网,基于互联网实现万物互联。 
- SE(Secure Element)安全元件,也称安全芯片,单独的防篡改硬件,专门用于存储和数据加密。 
- AT指令,终端设备和外设模块之间的连接和通信的指令。 
- ID²安全芯片,或ID²-SE芯片,在安全芯片生产时烧录ID²密钥,基于安全芯片的物理防护特性保护ID²密钥的存储和运行安全,通过SE AT指令提供ID²密钥的基础运算。 
- 阿里云物联网平台(Link Platform)阿里云物联网平台,提供物联网的设备管理。 
2. ID²产品架构

- ID²控制中心: 
- ID²的Web控制台,提供对ID²产线灌装、ID²产品和配额申请、以及ID²使用统计的管理。 
- ID²服务中心: 
- ID²在云端的应用,提供ID²的各种安全能力,包括ID²密钥安全分发、设备认证、基于ID²的安全连接协议等;同时,提供各种能力的云端接口,支持业务平台的二次开发,支持不同的安全业务需求和场景。 
- ID² Client SDK: 
- ID²在设备端的功能组件和软件开发框架,可支持不同操作系统和不同硬件,为IoT设备提供基于ID²的端到端的设备认证、数据加解密等各种安全能力。 
3. ID²接入流程
3.1 概述:

- 阿里云物联网平台接入的方式,ID²已默认同物联网平台打通,因此不需要进行服务端对接。 
- 设备建连的过程,IoT设备通过Paho MQTT Client SDK调用ID²-iTLS,进行设备认证和会话密钥协商,最后建立数据安全传输通道。 
- 设备建连成功后,IoT设备和物联网平台,通过安全通道进行应用数据的安全传输。 
- ID²对接步骤如下: 

3.2 创建产品:
- 登录物联网平台控制台。 
- 点击选择的实例,在左侧导航栏,选择设备管理 > 产品,单击创建产品: 

- 填写产品信息,认证方式选择ID²: 

3.3 购买ID²安全芯片:
- 购买ID²安全元器件: 
- 点击购买链接 - ID²安全元器件。 
- 选择硬件类型、型号和数量: 

- 分配ID²认证授权: 
2.1 登录到ID²控制台,进入入门指引 - IoT设备身份认证页面,选择“与阿里云物联网平台组合使用”,点击“开始接入”。
2.2 在“配置产品“步骤中,“选择产品”,然后“分配ID²授权数量”。

3.4 设备端对接:
3.4.1 ID² SDK框架:

- IoT Application: 
- 设备端的应用程序,负责业务数据处理,包括设备认证、设备建连和数据收发等。 
- Paho MQTT Client SDK: 
- Eclipse Paho MQTT C/C++ Client开源代码实现,提供基于MQTT协议的数据管理。 
- ID² Client SDK: 
- iTLS:轻量的安全连接协议TLS,基于ID²完成TLS的握手认证和密钥协议,提供应用数据的收发。 
- ID²:IoT设备认证的对外接口,上层应用/协议基于此接口进行开发。 
- KM:密钥管理模块,支持不同形式的载体: 
- Soft-KM:软件沙箱,基于软件加固和虚拟化技术提供对ID²密钥的安全保护。 
- SE:安全芯片,基于物理防护机制,提供对ID²密钥的安全保护,通过AT指令对设备提供ID²的运算。 
- Crypto:提供统一的加解密算法接口。 
- OSA:操作系统适配接口,厂商需根据使用的OS,重新进行接口适配。 
- HAL:硬件适配接口,提供算法库和Soft-KM的适配接口,厂商需根据选择的硬件平台,重新进行接口适配。 
3.4.2 ID² 设备端SDK获取:
- ID² SDK下载: 
- ID² Release Package目录: 
| 目录/文件 | 说明 | 
| app | 测试用例,包括HAL和ID² | 
| include | 头文件 | 
| libs | ID²模块的静态库 | 
| makefile | 编译脚本 | 
| make.rules | 编译系统配置文件,可配置编译工具链和编译参数 | 
| sample | ID²的示例代码 | 
| src | 需适配的OSA和HAL接口和参考实现 | 
3.4.3 ID² 设备端SDK对接:
- 设备端适配: 
根据选择的OS和硬件平台,完成ID² SDK的移植。

- 基于ID² 开源SDK进行移植,OSA和IROT HAL需进行重新适配。 
- 第一步:OSA接口适配: 
- 实现src/osa/ls_osa.c中的接口: 
- 已提供Linux系统的参考实现,可只实现其中的基础接口和网络接口。 
- 第二步:IROT HAL接口适配: 
- 进入modules/irot/se目录。 
- 在chipset目录, 创建新增安全芯片的目录(如fm1280),复制template中全部文件到新增的目录,并修改makefile指定到新建的目录。 
- 打开chipset/fm1280/se_driver/se_driver.c, 适配SE芯片驱动接口: 
- se_open_session - SE初始化 
- se_transmit - 发送APDU指令,并获取response。 
- se_close_session - SE关闭。 
- 第三步:ID² SDK编译: 
- 修改ID² SDK根目录make.rules的编译脚本: 
- CROSS_COMPILE设置对应的编译工具链。 
- CFLAGS设置编译的配置参数。 
- 修改ID² SDK根目录make.settings的统一配置: 
- CONFIG_LS_ID2_ROT_TYPE :配置为SE。 
- CONFIG_LS_ID2_KEY_TYPE:配置为ID²安全芯片中的密钥类型。 
- CONFIG_LS_ID2_ECDP_TYPE:如ID²密钥类型为ECC时,配置对应的椭圆曲线参数。 
- 设备端集成: 
Paho MQTT调用设备端ID²的接口,完成相应的设备认证、数据加密等安全功能。
本文通过在Linux系统,演示如何集成和使能ID²的认证和数据加密能力
- 第一步:下载Paho MQTT代码: 
- Paho MQTT Embeded-c代码下载: 
- 第二步:MQTT集成ID²的安全通道: 
- MQTTClient-C/src目录,在Linux下创建libs,拷贝已适配好的ID²静态库和头文件。 
- linux/MQTTLinux.h - 更新Network结构体和接口: 
typedef struct Network
{
        char *product_key;
        char *product_secret;
        uintptr_t handle;
        int (*mqttread) (struct Network*, unsigned char*, int, int);
        int (*mqttwrite) (struct Network*, unsigned char*, int, int);
} Network;
int mqtt_itls_read(Network*, unsigned char*, int, int);
int mqtt_itls_write(Network*, unsigned char*, int, int);
DLLExport void NetworkSetConfig(Network *, char *, char *);
DLLExport void NetworkInit(Network*);
DLLExport int NetworkConnect(Network*, char*, int);
DLLExport void NetworkDisconnect(Network*);
- Network结构体: 
- 增加product_key成员:阿里云物联网平台(LP)- 产品标识 
- 增加product_secret成员:阿里云物联网平台(LP)- 产品密钥 
- 变更网络句柄类型:my_socket(int) -> handle(uintptr_t) 
- Network网络接口: 
- 增加NetworkSetConfig:用于配置LP产品标识和产品密钥 
- linux/MQTTLinux.c - 适配Network网络接口: 
#include "hal_itls.h"
int mqtt_itls_read(Network* n, unsigned char* buffer, int len, int timeout_ms)
{
    return hal_itls_read(n->handle, buffer, len, timeout_ms);
}
int mqtt_itls_write(Network* n, unsigned char* buffer, int len, int timeout_ms)
{
    return hal_itls_write(n->handle, buffer, len, timeout_ms);
}
void NetworkSetConfig(Network* n, char *product_key, char *product_secret)
{
    n->product_key = product_key;
    n->product_secret = product_secret;
}
void NetworkInit(Network* n)
{
    n->handle = 0;
    n->mqttread = mqtt_itls_read;
    n->mqttwrite = mqtt_itls_write;
}
int NetworkConnect(Network* n, char* addr, int port)
{
    n->handle = hal_itls_establish(addr, port, n->product_key, n->product_secret);
    if (n->handle == 0) {
        return -1;
    }
    return 0;
}
void NetworkDisconnect(Network* n)
{
    hal_itls_destroy(n->handle);
}- CMakeLists.txt - 链接ID²静态库: 
file(GLOB SOURCES "*.c" "linux/*.c")
add_library(
  paho-embed-mqtt3cc SHARED
  ${SOURCES}
)
install(TARGETS paho-embed-mqtt3cc DESTINATION /usr/lib)
add_library(libitls STATIC IMPORTED)
set_target_properties(libitls PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libitls.a)
add_library(libid2 STATIC IMPORTED)
set_target_properties(libid2 PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libid2.a)
add_library(libicrypt STATIC IMPORTED)
set_target_properties(libicrypt PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libicrypt.a)
add_library(libkm STATIC IMPORTED)
set_target_properties(libkm PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libkm.a)
add_library(libls_hal STATIC IMPORTED)
set_target_properties(libls_hal PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libls_hal.a)
add_library(libls_osa STATIC IMPORTED)
set_target_properties(libls_osa PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/linux/libs/libls_osa.a)
target_link_libraries(paho-embed-mqtt3cc libitls libid2 libicrypt libkm libls_hal libls_osa)
target_link_libraries(paho-embed-mqtt3cc rt pthread)
target_include_directories(paho-embed-mqtt3cc PRIVATE "linux" "linux/include/itls")
target_link_libraries(paho-embed-mqtt3cc paho-embed-mqtt3c)
target_compile_definitions(paho-embed-mqtt3cc PRIVATE
             MQTTCLIENT_PLATFORM_HEADER=MQTTLinux.h MQTTCLIENT_QOS2=1)- 第三步:Paho通过ID²连接LP的示例 - MQTTClient-C/samples: 
- linux/aiot_id2_demo.c示例代码: 
#include "MQTTClient.h"
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#define EXAMPLE_PRODUCT_KEY         "xxxxxxx"
#define EXAMPLE_PRODUCT_SECRET      "xxxxxxx"
#define EXAMPLE_DEVICE_NAME         "Paho_ID2_Device_1"
#define EXAMPLE_DEVICE_SECRET       "b21bc6147266242838d06ff50c00d0ab"
#if !defined(CONFIG_LP_INSTANCED)
#define MQTT_CLINETID_KV            "|securemode=8,signmethod=hmacsha1,timestamp=2524608000000,authtype=id2|"
#else
#define MQTT_CLINETID_KV            "|securemode=8,signmethod=hmacsha1,timestamp=2524608000000,authtype=id2,instanceId=xxxxx|"
#endif
#define PRODUCTKEY_MAXLEN           (20)
#define DEVICENAME_MAXLEN           (32)
#define DEVICESECRET_MAXLEN         (64)
#define USERNAME_MAXLEN             (64)
#define PASSWORD_MAXLEN             (65)
int aiotMqttSign(const char *productKey, const char *deviceName, const char *deviceSecret, 
                     char clientId[150], char username[64], char password[65])
{
    char deviceId[PRODUCTKEY_MAXLEN + DEVICENAME_MAXLEN + 2] = {0};
    /* setup deviceId */
    memcpy(deviceId, deviceName, strlen(deviceName));
    memcpy(deviceId + strlen(deviceId), "&", strlen("&"));
    memcpy(deviceId + strlen(deviceId), productKey, strlen(productKey));
    /* setup clientid */
    memcpy(clientId, deviceId, strlen(deviceId));
    memcpy(clientId + strlen(deviceId), MQTT_CLINETID_KV, strlen(MQTT_CLINETID_KV));
    memset(clientId + strlen(deviceId) + strlen(MQTT_CLINETID_KV), 0, 1);
    /* setup username */
    memcpy(username, deviceId, strlen(deviceId));
    memset(username + strlen(deviceId), 0, 1);
    /* setup password */
    memset(password, '0', PASSWORD_MAXLEN - 1);
    memcpy(password, "FFFFFFFFFFFFFFFF1234567812345678", 32);
    return 0;
}
volatile int toStop = 0;
void cfinish(int sig)
{
    signal(SIGINT, NULL);
    toStop = 1;
}
void messageArrived(MessageData* md)
{
    MQTTMessage* message = md->message;
    printf("%.*s\t", md->topicName->lenstring.len, md->topicName->lenstring.data);
    printf("%.*s\n", (int)message->payloadlen, (char*)message->payload);
}
/* main function */
int main(int argc, char** argv)
{
    int rc = 0;
    /* setup the buffer, it must big enough for aliyun IoT platform */
    unsigned char buf[1000];
    unsigned char readbuf[1000];
    Network n;
    MQTTClient c;
    char *host = EXAMPLE_PRODUCT_KEY".itls.cn-shanghai.aliyuncs.com";
    short port = 1883;
    const char *subTopic = "/"EXAMPLE_PRODUCT_KEY"/"EXAMPLE_DEVICE_NAME"/user/get";
    const char *pubTopic = "/"EXAMPLE_PRODUCT_KEY"/"EXAMPLE_DEVICE_NAME"/user/update";
    /* invoke aiotMqttSign to generate mqtt connect parameters */
    char clientId[150] = {0};
    char username[65] = {0};
    char password[65] = {0};
    if ((rc = aiotMqttSign(EXAMPLE_PRODUCT_KEY, EXAMPLE_DEVICE_NAME,
                           EXAMPLE_DEVICE_SECRET, clientId, username, password) < 0)) {
        printf("aiotMqttSign -%0x4x\n", -rc);
        return -1;
    }
    printf("clientid: %s\n", clientId);
    printf("username: %s\n", username);
    printf("password: %s\n", password);
    signal(SIGINT, cfinish);
    signal(SIGTERM, cfinish);
    /* network init and establish network to aliyun IoT platform */
    NetworkInit(&n);
    NetworkSetConfig(&n, EXAMPLE_PRODUCT_KEY, EXAMPLE_PRODUCT_SECRET);
    rc = NetworkConnect(&n, host, port);
    if (rc < 0) {
        printf("NetworkConnect %d\n", rc);
        return -1;
    }
    /* init mqtt client */
    MQTTClientInit(&c, &n, 1000, buf, sizeof(buf), readbuf, sizeof(readbuf));
 
    /* set the default message handler */
    c.defaultMessageHandler = messageArrived;
    /* set mqtt connect parameter */
    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;       
    data.willFlag = 0;
    data.MQTTVersion = 3;
    data.clientID.cstring = clientId;
    data.username.cstring = username;
    data.password.cstring = password;
    data.keepAliveInterval = 60;
    data.cleansession = 1;
    printf("Connecting to %s %d\n", host, port);
    rc = MQTTConnect(&c, &data);
    if (rc < 0) {
        printf("MQTTConnect fail %d!\n", rc);
        return -1;
    } else {
        printf("MQTTConnect %d, Connect aliyun IoT Cloud Success!\n", rc);
    }    
    printf("Subscribing to %s\n", subTopic);
    rc = MQTTSubscribe(&c, subTopic, 1, messageArrived);
    printf("MQTTSubscribe %d\n", rc);
    int cnt = 0;
    unsigned int msgid = 0;
    while (!toStop)
    {
        MQTTYield(&c, 1000);	
        if (++cnt % 5 == 0) {
            MQTTMessage msg = {
                                QOS1, 0, 0, 0, "Hello world", strlen("Hello world"),
                              };
            msg.id = ++msgid;
            rc = MQTTPublish(&c, pubTopic, &msg);
            printf("MQTTPublish %d, msgid %d\n", rc, msgid);
        }
    }
    printf("Stopping\n");
    MQTTDisconnect(&c);
    NetworkDisconnect(&n);
    return 0;
}- 更新如下的信息: 
- EXAMPLE_PRODUCT_KEY:填写阿里云物联网平台(LP)产品标识,在创建时需选择“ID²认证” 
- EXAMPLE_PRODUCT_SECRET:填写阿里云物联网平台(LP)产品密钥,在产品创建控制台查询 
- EXAMPLE_DEVICE_NAME:填写设备标识,产品维度内唯一 
- EXAMPLE_DEVICE_SECRET:填写设备密钥,选择“ID²认证”时,可以使用任意字符串 
- 选择正确的MQTT_CLINETID_KV:ClientId的键值信息 
- LP公共实例: 
- deviceId+"|securemode=8,signmethod=hmacsha1,timestamp=2524608000000,authtype=id2|" 
- LP企业实例: 
- "|securemode=8,signmethod=hmacsha1,timestamp=2524608000000,authtype=id2,instanceId=xxxxx|", 其中instanceId为企业实例ID(登录物联网平台控制台,在实例概览页面查看) 
- LP平台的域名和端口号: 
- host:${YourProductKey}.itls.cn-shanghai.aliyuncs.com,其中${YourProductKey}为申请的产品标识 
- port:1883 
- CMakeLists.txt - 编译脚本: 
add_executable(
  stdoutsubc
  stdoutsub.c
)
target_link_libraries(stdoutsubc paho-embed-mqtt3cc paho-embed-mqtt3c)
target_include_directories(stdoutsubc PRIVATE "../../src" "../../src/linux")
target_compile_definitions(stdoutsubc PRIVATE MQTTCLIENT_PLATFORM_HEADER=MQTTLinux.h)
add_executable(
  aiot_id2_demo
  aiot_id2_demo.c
)
target_link_libraries(aiot_id2_demo paho-embed-mqtt3cc paho-embed-mqtt3c)
target_include_directories(aiot_id2_demo PRIVATE "../../src" "../../src/linux" "./include/osa")
target_compile_definitions(aiot_id2_demo PRIVATE MQTTCLIENT_PLATFORM_HEADER=MQTTLinux.h)
- 第四步:固件编译: 
- paho.mqtt.embedded-c根目录,创建build.paho编译目录 
- 到build.paho目录,运行"cmake & make"进行编译 
- 编译成功,ID²固件生成在build.paho/MQTTClient-C/samples/linux目录 
- 第五步:固件运行: 
- 正确运行aiot_id2_demo,可得到如下日志: 

3.5 调试验证:
- ID²建连错误调试: 
- ID²-iTLS建连失败时,首先可以通过查看消息警告(alert message)进行问题排查: 

- 上面的消息警告中,2 - FATAL类型的警告;172 - 消息警告类型,ID²-iTLS常见的错误警告类型如下: 
| 错误码 | 错误信息 | 评论 | 
| 160 | ID² generic error | 通用错误,检查Product Secret是否正确 | 
| 161 | ID² no quota | ID²配额不足,需先购买ID² | 
| 162 | ID² is not exist | 服务端识别不了此ID²,可删除设备上ID²(如KM载体)后,重新空发ID² | 
| 163 | ID² authcode is invalid | ID²认证码错误,校验挑战字是否有效(ID²服务端申请&只使用一次) | 
| 164 | ID² has not been activated | ID²已经激活 | 
| 165 | The timestamp used in authcode is expired | 检查设备端时间是否同步 | 
| 166 | ID² challenge is invalid | ID²认证码中,挑战字非法(挑战字模式) | 
| 167 | Not support this operation | 不支持此操作 | 
| 168 | ID² has been suspended | ID²已暂停使用 | 
| 169 | ID² has been discarded | ID²已废弃 | 
| 170 | Permission denied, id2 has been blinded to other product key | ID²已绑定到其他产品,不容许在该产品使用;改回绑定的产品,或者删除设备上的ID²,再重新空发一个新的ID² | 
| 171 | Product key is invalid | ID²产品非法,如已废弃 | 
| 172 | Product key is not exist | ID²产品不存在,产品错误填入,或者申请产品时,没有选择"ID²认证" | 
- ID²设备上线验证: 
- 设备端ID²-iTLS建连成功后,在物联网管理平台,可看到新创建的设备和设备的上线信息。 
- 登录物联网平台控制台。 
- 在左侧导航栏,选择设备管理 > 设备,DeviceName是ID²示例中设置的设备证书(ProductKey、DeviceName、DeviceSecret)设备名。 
- 登录IoT安全中心。 
- 在左侧导航栏,选择物联网身份 > 设备身份认证,查看通过ID²激活的设备信息。