前言
背景介绍:由于各种原因,阿里云用户有的设备不能使用Link SDK来接入物联网平台,需要自己编写代码实现mqtt sign,本文将初步的介绍一下签名的原理以及提供几个用代码实现mqtt sign的示例。
基本概念和原理:
本文调试用的工具地址: 调试工具
1、 加密算法:
1-1 散列算法(哈希、摘要)
也叫做Hash函数,常见的散列函数有MD5、SHA-1、SHA256等。
通过散列函数,可以为数据创建“数字指纹”(散列值、数字摘要)。散列值通常是一个短的随机字母和数字组成的字符串。信息收发双方在通信前商定了具体的散列算法,并且该算法是公开的。如果消息在传递过程中被篡改,则该消息不能与已获得的数字指纹相匹配。
注意:散列函数的主要作用不是完成数据加密和解密的工作,是用来验证数据的完整性的重要技术。
应用场景:A->B,B要验证A发送的内容没有被篡改
A:
原文1:123456
通过哈希函数(如:MD5)生成指纹1:e10adc3949ba59abbe56e057f20f883e
B:
收到原文2:1234567 (假设原文内容被中间人更改了)
把原文2通过MD5生成指纹2:fcea920f7412b5da7be0cf42b8c93759
通过对比指纹1和指纹2来是否一样,来判断原文内容是否被修改,而不是把指纹2逆向解析出原文(MD5不可逆)
特点:
单向不可逆:只能从输入推导出输出,而不能从输出计算出输入
可破解(不安全):
①、可以反查询
②、不可抵御中间人的攻击
高效,固定长度:散列算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出散列值。散列函数通过换算可以将任何长度的数据输出成固定长度的散列值。
(MD5 是128bit -16个字节-32个16进制的字符。)
雪崩效应:如果数据在传输过程中哪怕发生了一比特的修改,经过散列函数运算后,散列值会发生很大的变化。
1-2 HMAC
HMAC算法是基于信息散列算法的基础上,增加了一个密钥对散列算法的结果(指纹)进行加密得到密文1,
接收方也将收到的消息内容,通过同一个密钥对消息内容进行HAMC得到密文2,来比对密文1和密文2是否相同。在散列算法的基础上,增加了一个身份识别的功能。
常见的HMAC算法有:hmacmd5,hmacsha1和hmacsha256,对应散列算法中的md5,sha-1,sha-256。
优点:
优化了散列算法无法抵御中间人攻击的缺点。
假设有 A 、B、C 三个人。
业务场景是A给B要发送一段文字:"I Love You"
单纯使用散列算法的情况:
A发送给B:
原文1:I Love You
MD5指纹1:cd2824bba5d2803793c0553c8f7c0bd1
这个时候C从中作梗,给B发送
C发送给B
原文2:I Hate You
MD5指纹2:03a161dc954eb252606b24121b9ba801
B:收到原文2,通过原文2进行MD5得到指纹3,与指纹2进行比对发现内容完整
得到未被篡改的消息内容【I Hate You】
使用HMAC算法的情况:
A发送给B:
原文1:I Love You
将原文1用密钥1314520
进行HMAC运算得到密文1(签名1):5decea54c0a2f2e2a42f630fb90a6cfd
这个时候C想从中作梗,给B发送
C发送给B
原文2:I Hate You
但是由于没有密钥1314520,使用了其他密钥3344520
进行HMAC运算得到密文2(签名2):eeebed42f8a68907b3d9924c42d82749
B:收到原文2,通过密钥1314520进行HMAC运算
得到密文3(签名3):4aca2934ac46b546a5365f673f272c4c
通过比对密文2和密文3 不匹配,发现消息内容被篡改,成功抵御中间人C的攻击。
缺点:
1、发送方和接收方都要知道这个密钥,上文中的1314520
2、无法证明发送方是自己本人,密钥K如果泄露给了别人,那别人就可以使用密钥K与接收方进行正常通信
HMAC可以增加身份识别,但是存在身份可靠性问题,无法验证该身份是否合法。
(如何解决:CA机构+TLS证书,相当于【公安机关+身份证】。本文不作过多讲解。)
1-3 数字签名
数字签名就是在散列算法的基础上,使用一定的加密算法,进行加密得到别人无法伪造的一段数字串。
1-2中的HMAC也属于数字签名的一种运用,另外像TLS证书上的签名等等也是数字签名的运用,只不过TLS证书
使用的是非对称加密方式,通过公钥解密进行验签,HMAC使用的是对称加密方式,通过比对签名的方式进行验签。
用户A用私钥将原文打上自己的签名,用户B通过用户A发布到网上的公钥(证书)对签名进行解密(验签)来确定用户A的身份以及消息内容是否被更改。
2、MQTT签名机制
2-1概述
mqtt签名实际就是mqtt设备接入(包括一机一密认证、一型一密预注册认证、一型一密预注册、一型一密免预注册)物联网平台CONNECT报文中的mqttPassword这个参数,也就是mqtt接入的密码,物联网平台用来校验设备的身份。
另外,注意,一型一密免预注册添加的设备进行认证,不需要签名,直接使用的是token进行认证
从文档中可以看到,这里的签名方法使用的是HMAC的方式
signmethod:表示签名算法类型。支持hmacmd5,hmacsha1和hmacsha256,默认为hmacmd5。
结合1-2章节HMAC,mqtt签名机制,实际上就是:设备端将连接报文中的一部分参数当成消息内容,通过HMAC算法
签名生成一个密码,服务器端从收到的报文按照约定也通过HMAC算法签名生成一个密码,进行校验。
2-2设备端(客户端)
注意:mqttClientId和clientId以及ClientID的区别,clientId是mqttClientId的一部分,clientId=ClientID
clientId可以是自定义的,ClientID是由物联网平台提供(一型一密免预注册)。
设备端通过发送MQTT连接报文接入物联网平台,包含三个参数:
(1)、mqttClientId:
认证 |
注册 |
||||
控制台注册/ |
免预注册方式添加设备 |
预注册 |
免预注册 |
||
一机一密 |
老版公共实例 |
clientId+"|securemode=3, |
不支持 |
不支持 |
|
新版公共实例 |
|||||
企业实例 |
|||||
一型一密 |
老版公共实例 |
clientId+"|securemode=-2, (这里的clientId非自定义了,整个mqttClientID都是通过免预注册一起返回来的) |
clientId+"|securemode=2, |
clientId+"|securemode=-2, |
|
新版公共实例 |
clientId+"|securemode=2, |
clientId+"|securemode=-2, |
|||
企业实例 |
(2)、mqttUsername:
deviceName+"&"+productKey
(3)、mqttPassword:
一机一密认证:
将productKey、deviceName、timestamp和clientId几个参数生成content
一型一密:
将deviceName、productKey、random 几个参数生成content
使用密钥(DeviceSecret或者ProductSecret)进行HMAC 算法签名得到mqttPassword
一机一密用DeviceSecret,一型一密用ProductSecret。
顺便区分一下一机一密和一型一密:
一机一密,是用于激活设备,创建和物联网平台的mqtt通信通道,
一型一密,是用于注册设备,获取设备的接入信息(设备三元组或者ClientID、DeviceToken)
一型一密分为预注册和免预注册,预注册的方式得到的是三元组信息,再通过一机一密的方式激活设备。
免预注册的方式得到的是ProductKey、DeviceName、ClientID、DeviceToken,直接通过这几个参数接入物联网平台。
(DeviceToken 作为mqttPassword,所以设备端不需要再计算mqttPassword)
2-3物联网平台(服务器)
物联网平台从MQTT报文中的(productKey、deviceName、timestamp和clientId)或者(deviceName、productKey、random)几个参数生成content
从MQTT报文中的signmethod参数,得到HMAC算法,
通过该算法计算签名得到sign来和MQTT报文中的mqttPassword参数进行比对,达到设备身份校验的目的。
2-4如何规避HAMC的缺点
1-2中提到,HMAC,双方都需要"提前”知道密钥,虽然可以校验身份,但是对发送方的身份合法性不能进行校验。
(1)所以设备密钥(DeviceSecret),产品密钥(ProductSecret)这个东西不要泄露给其他人,文档中也有明确提示。只有设备端和平台“知道”,就保证了身份的合法性。
(2)设备和物联网平台建立连接的时候也可以选择TLS接入,即MQTT-TLS,利用TLS证书加强对身份合法性的校验,本文不过多描述。
实践示例
1、使用阿里云现有工具
参考链接,不过多叙述
2、使用C语言进行mqtt sign。
2-1 概述-准备工作
(1)环境说明
- 开发语言:C99标准的C语言。
- 开发工具:Linux 或者Mac OS ,VIM编辑器
- 编译工具:GCC
(2)前提条件
已在物联网平台控制台,对应实例下,创建产品和设备,并获取MQTT接入域名和设备证书信息(ProductKey、DeviceName和DeviceSerect)。具体操作,请参见:
(3)源码获取
单击打开aiot_mqtt_sign.c,复制源码,然后粘贴保存为本地的aiot_mqtt_sign.c文件。aiot_mqtt_sign.c文件定义了函数aiotMqttSign()。
- 原型:intaiotMqttSign(constchar *productKey, constchar *deviceName, constchar *deviceSecret, char clientId[150], char username[65], char password[65]);
2-2 修改源码,自定义设备的mqtt接入参数
(1) 增加stdint.h头文件的包含
里面有uint32_t 这种数据类型的定义。
(2)编写main函数(程序入口)
int main()
{
/* invoke aiotMqttSign to generate mqtt connect parameters */
char clientId[150] = {0};
char username[65] = {0};
char password[65] = {0};
int rc = 0;
#if MQTTAUTH
/*一机一密*/
if ((rc = aiotMqttSign(EXAMPLE_PRODUCT_KEY, EXAMPLE_DEVICE_NAME, EXAMPLE_DEVICE_SECRET, clientId, username, password) < 0)) {
printf("aiotMqttSign -%0x4x\n", -rc);
return -1;
}
#else
/*一型一密*/
if ((rc = aiotMqttSign(EXAMPLE_PRODUCT_KEY, EXAMPLE_DEVICE_NAME, EXAMPLE_PRODUCT_SECRET, clientId, username, password) < 0)) {
printf("aiotMqttSign -%0x4x\n", -rc);
return -1;
}
#endif
printf("mqttClientId: %s\n", clientId);
printf("mqttUsername: %s\n", username);
printf("mqttPassword: %s\n", password);
return rc;
}
(3)增加宏定义 MQTTAUTH、REGISTER、REGNWL
/*mqtt 认证*/
#define MQTTAUTH
/*一型一密预注册*/
#define REGISTER
/*一型一密预注册*/
#define REGNWL
(4)增加宏定义EXAMPLE_PRODUCT_KEY 和 EXAMPLE_DEVICE_CLIENTID
#define EXAMPLE_PRODUCT_KEY "a16hDZJpRCl"
#define EXAMPLE_DEVICE_CLIENTID "qwert"
(5) 删除这两行代码
2-2-1 一机一密
① 注释掉REGISTER、REGNWL宏,留下MQTTAUTH
② 初始化content、mqttClientid所需要的值
(一机一密content要用到productKey、deviceName、timestamp和clientId)。
ProductKey、DeviceName、DeviceSecret、timestamp。
#ifdef MQTTAUTH
/*一机一密认证/控制台(API)添加的设备的认证*/
#define EXAMPLE_DEVICE_NAME "IoTDeviceDemo1"
#define EXAMPLE_DEVICE_SECRET "a3b15a116aec5**马赛克**39b7bd4bc4952"
#define TIMESTAMP_VALUE "1655198877533"//当前时间
#define MQTT_CLINETID_KV "|timestamp=1655198877533,_v=paho-c-1.0.0,securemode=3,signmethod=hmacsha256,lan=C|"
#endif
其中,MQTT_CLINETID_KV ,即构造mqttClientId参数
由clientid+ MQTT_CLINETID_KV组成
修改MQTT_CLINETID_KV,
timestamp要和TIMESTAMP_VALUE对应
signmethod 固定写死hmacsha256,目前只实现了这个算法。
③ 修改源码使clientId实现自定义
增加两行
char ClientID[1024] = {0};
memcpy(ClientID, EXAMPLE_DEVICE_CLIENTID,strlen(EXAMPLE_DEVICE_CLIENTID));
修改三行
memcpy(clientId, ClientID, strlen(ClientID));
memcpy(clientId + strlen(ClientID), MQTT_CLINETID_KV, strlen(MQTT_CLINETID_KV));
memset(clientId + strlen(ClientID) + strlen(MQTT_CLINETID_KV), 0, 1);
如图:
④ 构造mqttUsername
(无需修改)
固定DeviceName&ProductKey的格式
⑤ 按字典升序组装content
修改这一段代码为
#ifdef MQTTAUTH
memcpy(macSrc, "clientId", strlen("clientId"));
memcpy(macSrc + strlen(macSrc), ClientID, strlen(ClientID));
memcpy(macSrc + strlen(macSrc), "deviceName", strlen("deviceName"));
#else
memcpy(macSrc, "deviceName", strlen("deviceName"));
#endif
memcpy(macSrc + strlen(macSrc), deviceName, strlen(deviceName));
memcpy(macSrc + strlen(macSrc), "productKey", strlen("productKey"));
memcpy(macSrc + strlen(macSrc), productKey, strlen(productKey));
#ifdef MQTTAUTH
memcpy(macSrc + strlen(macSrc), "timestamp", strlen("timestamp"));
memcpy(macSrc + strlen(macSrc), TIMESTAMP_VALUE, strlen(TIMESTAMP_VALUE));
#else
memcpy(macSrc + strlen(macSrc), "random", strlen("random"));
memcpy(macSrc + strlen(macSrc), RANDOM_VALUE, strlen(RANDOM_VALUE));
#endif
clientIdqwertdeviceNameIoTDeviceDemo1productKeya16hDZJpRCltimestamp1655198877533
添加一行:加个content的打印,调试用,content没有组装对也会导致签名错误。(易错点)
printf("content:%s\n",macSrc);
⑥ 使用设备密钥(DeviceSecret)对字典升序后的content进行签名,构造mqttPassword参数
(无需修改代码)
调用utils_hmac_sha256接口,进行hamcsha256运算,目前只实现了这个算法。
⑦ 编译&运行,验证结果
输入编译&运行指令:
gcc aiot_mqtt_sign.c -o test1
./test1
另一边,用工具计算出的结果:
对比一下工具计算出来的结果和mqttPassword,两者是完全匹配的。(这一步还不能完全说明签名是正确的,只能说代码逻辑没有问题,但是如果content不对,最终的签名和阿里云还是不匹配的。)
然后使用这三个参数,mqttClientId,mqttUsername,mqttPaassword验证是否可以通过阿里云物联网平台认证:
mqttClientId: qwert|timestamp=1655198877533,_v=paho-c-1.0.0,securemode=3,signmethod=hmacsha256,lan=C|
mqttUsername: IoTDeviceDemo1&a16hDZJpRCl
mqttPassword: 24F9D22DA8DE6D8BE6****(马赛克)****E982BB357DF6FBA037D769F
MQTT.FX用这几个参数成功接入,如图
到这一步才能说明签名是完全正确的。
2-2-2 一型一密预注册
注意检查产品的动态注册开关是否已打开。
在一机一密的代码基础上进行修改
① 注释掉MQTTAUTH 、REGNWL宏,留下REGISTER
② 增加ProductSecret和Radom的宏定义,DeviceName换一个设备(未激活状态),以及mqttClientId
#ifdef REGISTER
#define EXAMPLE_DEVICE_NAME "IoTDeviceDynamicDemo1"
#define EXAMPLE_PRODUCT_SECRET "oa7oO91coNaHT6OS"
#define RANDOM_VALUE "123456789"
/*一型一密预注册,这里用老版公共实例测试,如果是企业实例或者新版公共实例,需要加一个instanceId参数*/ #define MQTT_CLINETID_KV "|securemode=2,authType=register,random=123456789,signmethod=hmacsha256|"
#endif
其中,MQTT_CLINETID_KV ,即构造mqttClientId参数
由clientid+ MQTT_CLINETID_KV组成
修改MQTT_CLINETID_KV,
timestamp要和TIMESTAMP_VALUE对应
signmethod 固定写死hmacsha256,目前只实现了这个算法。
③ 构造mqttUsername
(无需修改)
固定DeviceName&ProductKey的格式
④ 按字典升序组装content
在一机一密的代码基础上,无需再改,注意前边的宏定义REGISTER
注意:一型一密认证的content不需要clientid和timestamp这两个参数,另外加了一个random参数
content:deviceNameIoTDeviceDynamicDemo1productKeya16hDZJpRClrandom123456789
⑤ 使用产品密钥(ProductSecret)对字典升序后的content进行签名,构造mqttPassword参数
(不需修改),注意前边的宏定义REGISTER
调用utils_hmac_sha256接口,进行hamcsha256运算,目前只实现了这个算法。
⑥ 编译&运行,验证结果
输入编译&运行指令:
gcc aiot_mqtt_sign.c -o test2
./test2
另一边,用工具计算出的结果:
对比一下工具计算出来的结果和mqttPassword,两者是完全匹配的。(这一步还不能完全说明签名是正确的,只能说代码逻辑没有问题,但是如果content不对,最终的签名和阿里云还是不匹配的。)
然后使用这三个参数,mqttClientId,mqttUsername,mqttPaassword验证是否可以通过阿里云物联网平台成功进行注册:
mqttClientId: qwert|securemode=2,authType=register,random=123456789,signmethod=hmacsha256|
mqttUsername: IoTDeviceDynamicDemo1&a16hDZJpRCl
mqttPassword: 102469CD77D18973AB6EB0E0****(马赛克)****BF9056767E529BBF5DEB4CE
MQTT.FX用这几个参数成功进行动态注册,如图
注意勾选SSL
到这一步才能说明签名是完全正确的。
2-2-3 一型一密免预注册
在一型一密预注册的代码基础上进行修改
① 注释掉MQTTAUTH 、REGISTER宏,留下REGNWL
② 增加ProductSecret和Radom的宏定义,以及mqttClientId,自定义一个DeviceName。
#ifdef REGNWL
/*一型一密免预注册,这里用老版公共实例测试,如果是企业实例或者新版公共实例,需要加一个instanceId参数*/ #define EXAMPLE_PRODUCT_SECRET "oa7oO**马赛克**aHT6OS"
#define EXAMPLE_DEVICE_NAME "IoTDeviceDynamicDemo2"
#define RANDOM_VALUE "123456789"
#define MQTT_CLINETID_KV "|securemode=-2,authType=regnwl,random=123456789,signmethod=hmacsha256|"
#endif
其中,MQTT_CLINETID_KV ,即构造mqttClientId参数
由clientid+ MQTT_CLINETID_KV组成
修改MQTT_CLINETID_KV,
timestamp要和TIMESTAMP_VALUE对应
signmethod 固定写死hmacsha256,目前只实现了这个算法。
③ 构造mqttUsername
(无需修改)
固定DeviceName&ProductKey的格式
④ 按字典升序组装content
在一型一密预注册的代码基础上,(无需修改),注意前边的宏定义REGNWL
注意:一型一密认证的content不需要clientid和timestamp这两个参数,另外加了一个random参数
content:deviceNameIoTDeviceDynamicDemo2productKeya16hDZJpRClrandom123456789
⑤ 使用产品密钥(ProductSecret)对字典升序后的content进行签名,构造mqttPassword参数
(不需修改),注意前边的宏定义REGISTER
调用utils_hmac_sha256接口,进行hamcsha256运算,目前只实现了这个算法。
⑥ 编译&运行,验证结果
输入编译&运行指令:
gcc aiot_mqtt_sign.c -o test3
./test3
另一边,用工具计算出的结果:
对比一下工具计算出来的结果和mqttPassword,两者是完全匹配的。(这一步还不能完全说明签名是正确的,只能说代码逻辑没有问题,但是如果content不对,最终的签名和阿里云还是不匹配的。)
然后使用这三个参数,mqttClientId,mqttUsername,mqttPaassword验证是否可以通过阿里云物联网平台成功进行免预注册:
mqttClientId: qwert|securemode=-2,authType=regnwl,random=123456789,signmethod=hmacsha256|
mqttUsername: IoTDeviceDynamicDemo2&a16hDZJpRCl
mqttPassword: 59ABF3B1E0C6FF841705393****马赛克****6F0CD1E6F578193D126ABB42
MQTT.FX用这几个参数成功进行免预注册,如图
注意勾选SSL
而且控制台上可以看到通过免预注册的成功添加的设备
到这一步才能说明签名是完全正确的。