LuatOS核心库RSA加解密与签名API实战指南
发布时间:2026/9/24 23:40:26来源:尧图网络
做物联网设备开发的朋友应该都对LuatOS不陌生它让合宙系模组可以用Lua脚本快速写出业务逻辑大大降低了嵌入式开发的门槛。不过脚本语言写起来爽真到了需要做设备身份认证、数据加密上报这些安全相关功能时很多人就开始挠头了——毕竟安全通信这块底层全是算法和密钥格式的活儿。这篇就来把LuatOS核心库里的【rsa】非对称加解密API彻底讲透从原理、API参数到实际可跑的代码一次说清楚。RSA属于非对称加密简单说就是公钥负责加密或验签私钥负责解密或签名两个密钥成对出现。在LuatOS的设备端场景里最常见的用法就是设备拿到服务器下发的公钥把采集的数据加密后上报服务器用私钥解密反过来设备用私钥对请求签名服务器用公钥验签确保请求确实来自合法设备。这篇内容适合正在做LuatOS设备端安全通信、对接云端API、或者想把固件升级包、控制指令做签名保护的开发者参考新手也能照着步骤跑通。1. LuatOS的RSA模块到底能干什么1.1 物联网设备里的典型应用场景很多跑LuatOS的模组比如Air780E、Air724UG这些都直接内置了RSA算法库不需要额外移植mbedtls直接require(rsa)就能用。我实际项目里RSA用得最多的是三个地方。第一是设备身份认证。设备第一次联网时服务器下发一段随机数挑战码设备用私钥对它签名后回传服务器用设备证书里的公钥验签。这样能确定对面确实是持有对应私钥的设备而不是被伪造的假设备。这个流程类似你去银行办业务柜员让你输密码证明“你就是你”。第二是敏感数据加密传输。比如设备上报的定位信息、用户隐私数据用RSA公钥加密后再走MQTT或HTTP通道即使链路被监听拿不到私钥的人也解不开内容。要注意RSA加密的数据长度有限制这后面会详细讲。第三是OTA固件包的完整性校验。固件包下发前用服务器私钥做签名设备下载完后用内置公钥验签确保固件没有被篡改。这个跟安全启动的思路一样防的就是固件被植入后门。1.2 LuatOS的RSA库和PC端有什么不一样用过Python的Crypto库或者OpenSSL的人上手LuatOS的RSA时最容易踩坑的就是密钥格式。PC端通常习惯用PEM格式也就是那种带-----BEGIN PUBLIC KEY-----头尾的Base64文本而LuatOS底层的mbedtls默认操作的是DER格式是二进制的直接拿着PEM文本去调rsa.encrypt大概率报错。另外一个差异是内存限制。在PC上生成2048位密钥、加密几KB数据都不是事但模组通常只有几百KB的RAM生成2048位密钥要反复做素数筛选和模幂运算耗时可能好几秒甚至更久期间内存峰值也很高。所以LuatOS设备端用1024位密钥比较常见虽然安全性弱一些但对MCU来说性能更现实。当然如果模组资源够且业务对安全等级要求高2048位也不是不能用只是要评估好超时和内存。2. RSA核心原理与参数选型2.1 用一句话讲清楚非对称加密对称加密就像一把钥匙开一把锁加密解密用同一个密钥非对称加密则是“一把锁配两把钥匙”公钥只能锁加密私钥只能开解密。公钥可以随便分发私钥必须自己收好。RSA具体的数学原理是建立在“大整数分解很难”这个基础上的取两个大素数p和q计算它们的乘积np*q再用n和某个指数e组成公钥n和另一个指数d组成私钥。理论上通过公钥(n, e)去反推私钥的d就必须分解出p和q而大数分解在计算上是不可行的所以算法安全。实际编程中你根本不需要关心这些数学细节只要记住三句话公钥加密私钥解密 → 用于机密性保护。私钥签名公钥验签 → 用于身份认证和完整性。公钥能加密的数据长度有限私钥能解密的数据长度也有限超长数据要先分段或者用混合加密。2.2 密钥长度和填充方式的取舍LuatOS的rsa.genKeyPair接口可以指定位数我实测下来1024位在Air780E上生成大约需要1到3秒2048位可能要到10秒以上。密钥越长越安全但性能和内存开销也随之翻倍。填充方式就更重要了。RSA本身只能加密比密钥短的纯数值所以标准里定义了填充方案把明文填充到密钥长度再加密。常见的有两种PKCS1 v1.5填充老牌方案简单兼容性好大部分场景够用。加密块最大长度 密钥位数/8 - 11。OAEP填充更安全加了随机因子同样的明文每次加密结果都不同能防选择明文攻击。但LuatOS老版本支持不完整用之前得确认固件版本。我用一个表格整理一下不同密钥长度配合PKCS1填充时的单次加密上限密钥位数密钥字节数单次最大加密明文长度PKCS1签名数据长度512 位64 字节53 字节64 字节1024 位128 字节117 字节128 字节2048 位256 字节245 字节256 字节很好理解一次RSA运算处理的块长度就是密钥字节数比如1024位密钥的模长就是128字节PKCS1填充至少要占11字节所以最多能塞117字节明文。超过这个长度必须分段或者干脆走AESRSA的混合加密方案。3. LuatOS RSA核心API逐个拆解3.1 rsa.genKeyPair生成密钥对这个接口是整套API的入口调用方式如下local rsa require(rsa) -- 生成1024位密钥对返回DER格式的公钥和私钥 local pubDER, priDER rsa.genKeyPair(1024) if pubDER and priDER then log.info(rsa, 公钥长度, #pubDER, 私钥长度, #priDER) end返回的pubDER和priDER都是二进制字符串可以直接存到文件系统里下次上电直接读取使用不必每次都重新生成。有两个细节必须提醒一下。第一生成密钥对在MCU上属于“重活”期间如果系统跑着其他任务一定要评估是否会造成任务调度卡顿。我建议在初始化阶段、确保网络连接还没建立时就先生成好避免业务高峰期才去做这个计算。如果密钥对是固定的直接在编译期放一份DER内容进固件更省事。第二私钥一定要安全存储。LuatOS部分模组提供了fota分区或加密存储区域私钥不要明文放在普通文件里防止固件被读出来后私钥也泄露了。安全设计里私钥泄露等于设备身份被夺所以这块别偷懒。3.2 rsa.encrypt和rsa.decrypt加解密加解密接口的用法很直接local rsa require(rsa) -- 公钥加密 local encData rsa.encrypt(pubDER, hello luatos) -- encData是二进制密文长度等于密钥字节数1024位即128字节 -- 私钥解密 local decData rsa.decrypt(priDER, encData) log.info(rsa, 解密结果:, decData)我特意把“公钥加密、私钥解密”这个顺序强调一下因为很多人第一次用会把参数传反。LuatOS的rsa.encrypt接口在传入密钥时内部会根据密钥类型自动判断行为传入公钥就是加密传入私钥有些版本则可以做私钥加密用于构造签名时用的PKCS1 v1.5 block type 1。实际对接服务器时最常见的场景是这样的服务器生成密钥对把公钥以DER或PEM格式下发到设备设备用这份公钥加密业务数据服务器用私钥解密。反过来设备用自己保存的私钥对关键指令做签名服务器用设备公钥验签。3.3 rsa.sign和rsa.verify签名验签签名验签是设备身份认证的核心LuatOS的接口长这样local rsa require(rsa) -- 用私钥对数据做SHA256签名 local signature rsa.sign(priDER, device id timestamp, SHA256) -- 用公钥验证签名返回true/false local ok rsa.verify(pubDER, device id timestamp, signature, SHA256) log.info(rsa, 验签结果, ok)第三个参数是哈希算法LuatOS底层常用的是MD5、SHA1、SHA224、SHA256、SHA384、SHA512。我强烈建议用SHA256MD5和SHA1都已经被证明存在碰撞风险用在签名场景里不安全。签名验签的典型业务逻辑是设备拼接一段“业务参数时间戳随机数”用私钥签名后连同原始数据一起发给服务器服务器用公钥验签。因为私钥只有设备持有所以收到合法签名就说明数据确实来自这台设备时间戳防重放随机数防同样的签名被多次提交。4. 完整实操设备端RSA加密上报到服务器4.1 服务端密钥生成与公钥下发我先模拟一套完整流程。假设服务器端用Python生成RSA密钥对然后把公钥以DER格式转成Base64字符串下发给设备。# 服务端 Python 脚本生成密钥并导出 from Crypto.PublicKey import RSA key RSA.generate(2048) # 导出公钥去掉PEM头尾得到Base64片段 public_key_pem key.publickey().export_key(PEM) # 也可以直接导出DER二进制再转Base64 public_key_der key.publickey().export_key(DER) import base64 pub_b64 base64.b64encode(public_key_der).decode() print(私钥PEM:, key.export_key(PEM).decode()) print(公钥DER Base64:, pub_b64)为什么我这里生成的是2048位而刚才建议设备端用1024位因为这个流程里服务端负责解密2048位更安全且服务端性能足够设备端只用公钥做加密运算量比解密更小2048位公钥加密在MCU上通常也能扛住但如果追求性能和flash占用用1024位也说得过去两端密钥位数并不强制一致。严格来说公钥加密2048位数据的耗时比1024位高不少大家按实际业务并发评估。设备上电后通过MQTT或HTTP拿到这份Base64的公钥解Base64得到DER二进制就可以直接传给rsa.encrypt了。4.2 设备端加密上报流程实现设备端完整示例代码我写成这样local rsa require(rsa) local sys require(sys) local crypto require(crypto) -- 得到服务端下发的公钥DER此处省略网络获取代码以字符串示意 local pub_b64 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... local pubDER crypto.base64_decode(pub_b64) -- 上报数据 local payload json.encode({device_id A123, temp 26.5, ts os.time()}) log.info(rsa, 明文长度, #payload) -- RSA单次加密长度有限这里用AES混合加密随机AES密钥加密数据RSA加密AES密钥 local aes_key crypto.trng(16) -- 生成16字节AES密钥 local aes_iv crypto.trng(16) local enc_data crypto.aes_encrypt(aes_key, payload, aes_iv, CBC, PKCS7) local enc_aes_key rsa.encrypt(pubDER, aes_key) -- 把RSA加密后的AES密钥和AES密文一起上报 local report { key crypto.base64_encode(enc_aes_key), data crypto.base64_encode(enc_data), iv crypto.base64_encode(aes_iv) } -- 走mqtt或http上报 -- mqtt.publish(/device/upload, json.encode(report))这里我选择了AESRSA混合加密不是因为RSA不够强而是因为RSA单次加密能力有限且慢。实际业务数据动不动几百字节甚至几KB用RSA分段加密太笨重不如用AES加密数据、RSA保护AES密钥两全其美。AES在模组上速度飞快RSA只处理16字节的密钥毫无压力。如果你确实要对较短的数据直接用RSA加密比如上报一段不到100字节的状态字符串也可以直接调用rsa.encrypt但要自己处理分段逻辑下一节我会讲。4.3 服务器端解密验签的配对逻辑服务器收到设备上报的JSON后解析出key和data用私钥解出AES密钥再用AES密钥解出真正的数据。Python示例如下import base64 from Crypto.Cipher import AES, PKCS1_OAEP, PKCS1_v1_5 from Crypto.PublicKey import RSA # 服务器私钥 private_key RSA.import_key(open(private.pem).read()) # 解Base64并RSA解密出AES密钥 enc_aes_key base64.b64decode(report[key]) cipher_rsa PKCS1_v1_5.new(private_key) aes_key cipher_rsa.decrypt(enc_aes_key, sentinelNone) # 再用AES密钥解出数据 iv base64.b64decode(report[iv]) enc_data base64.b64decode(report[data]) cipher_aes AES.new(aes_key, AES.MODE_CBC, iv) payload cipher_aes.decrypt(enc_data).rstrip(b\x00)注意这里的填充方式必须和LuatOS加密端保持一致。LuatOS里如果用rsa.encrypt直接处理默认的填充是PKCS1 v1.5Python端的PKCS1_v1_5需要自己保证传入的待加密数据不超过245字节2048位密钥否则要分段。所以建议用混合加密省心又高效。签名验签的配对逻辑也类似设备把业务参数 时间戳 随机数拼好私钥签名后一起上报服务器先把原始串重新拼出来用设备公钥验签验签通过再处理业务。这个顺序千万别反先解密再验签或先验签再解密取决于你设计API时的分层但验签一定要在真正执行业务逻辑之前完成。5. 常见问题与排查技巧实录5.1 密钥格式报错与填充方式不匹配我把实际开发中踩过的坑排了个序第一个高频问题就是密钥格式不对。LuatOS的rsa.encrypt和rsa.decrypt期望收到DER二进制密钥如果你手里只有PEM文本必须做转换。在PC上可以用命令转# PEM公钥转DER openssl rsa -pubin -in public.pem -outform DER -out public.der # 或者用Python直接取DER python3 -c from Crypto.PublicKey import RSA; print(RSA.import_key(open(public.pem).read()).publickey().export_key(DER).hex())然后在LuatOS里通过crypto.base64_decode把Base64解码成二进制或者直接把DER的hex字符串用string.fromHex转成二进制。第二高频的问题是两端填充方式不一致。设备端用默认的PKCS1 v1.5加密服务器端如果用了PKCS1_OAEP去解密直接报错“Ciphertext length incorrect”或乱码。要统一填充方式按我刚才的示例走就不会错。5.2 分段加密的实现与推荐替代方案如果你非要直接用RSA加密超过单次限制的数据就得自己分段。参考思路是把明文按117字节1024位密钥或245字节2048位密钥切块逐块调用rsa.encrypt然后拼接所有密文解密端按密钥字节数切块逐块rsa.decrypt最后拼接明文。我写一个简单示意local function rsa_encrypt_long(pubDER, data, block_size) local result {} local len #data for offset 1, len, block_size do local part data:sub(offset, offset block_size - 1) result[#result1] rsa.encrypt(pubDER, part) end return table.concat(result) end但说句心里话我真的不建议在MCU上这么干。分段加密除了代码啰嗦还有一个麻烦是密文长度会变成每块密钥字节数的整数倍服务器端要知道你用的分段大小才能正确切分。更合理的方案永远是AESRSA混合加密一次RSA运算保护AES密钥再多数据都交给AES处理。这个方案在真实工业产品里是绝对的主流。5.3 性能、内存与超时设置的考量RSA解密在模组上是比较耗时的操作。我实测Air780E跑1024位RSA解密一次大约在几十毫秒到一百多毫秒2048位可能到数百毫秒。如果你的业务代码里有大量连发消息每个消息都做一次RSA解密CPU会明显吃紧这时候要做缓存或者限制频率。内存方面也要注意rsa.genKeyPair(2048)的瞬时内存占用可能达到几十KB甚至更多对于内存紧张的模组要么换1024位要么确保在业务初始化阶段调用而不是在内存紧张的任务里调用。另外rsa.decrypt返回的解密结果是新分配的字符串大块数据解密时要注意Lua的GC压力如果频繁解密大块数据可能出现内存碎片。网络请求的超时设置也要留足余量。尤其签名、解密这类操作叠加在MQTT回调线程里执行时如果超时设得太短可能因算法耗时导致回调超时表现为“设备偶尔掉线”或者“任务不响应”。5.4 常见错误速查表我整理了一张排查表基本都是群里问得最多的问题现象可能原因解决办法rsa.encrypt返回nil数据长度超限或密钥格式错误检查数据长度是否超过密钥单次上限密钥是否DER格式解密出来是乱码两端填充方式不一致统一使用PKCS1 v1.5或统一使用OAEPrsa.verify一直返回false签名数据拼接方式不一致确保验签端用完全相同的原始串顺序、分隔符都要一样生成密钥对时程序重启或卡死堆栈不足或超时改用1024位放在初始化阶段执行调大任务栈服务器报“invalid padding”RSA密文被截断或密钥不匹配检查网络上传输时Base64是否有换行、空格被剥离5.5 一个容易忽略的小技巧最后分享一个我自己的习惯在做RSA加密或签名之前先把原始数据里的换行、空格、制表符全部清干净尤其是从配置文件或者网络接口拼接出来的字符串。很多“加密正常但服务器就是验签失败”的诡异问题最后查出来都是拼接时多了一个\r\n。为了定位这类问题我一般会在设备端把待签名的原始串打一条日志服务器端也把收到的原始串打一条日志肉眼比对一遍问题马上现形。另外一个细节是RSA加密输出的密文是二进制直接打印会乱码也不适合做日志。调试时记得转成hex或Base64再打印log.info(rsa, encData hex, encData:toHex())不然日志里一堆乱码排错体验很差。LuatOS的RSA API其实很简单复杂的是两端配对时的格式和参数一致性。按照这篇的思路先定好密钥长度和填充方式再选直接RSA分段还是AESRSA混合方案然后联调时逐项核对格式基本不会出大问题。我实际项目里跑下来只要在密钥格式和填充方式这两处把关后续的加解密流程都挺顺畅的。希望这份总结能帮你少踩几个坑。
网站建设高端定制企业官网