拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32上跑RSA:从mbedtls移植到node-forge联调全记录

简介本资源是一个面向嵌入式安全开发者的STM32平台RSA2048非对称加密解密完整实现工程适用于需在资源受限MCU上部署高安全性通信协议的工程师与进阶学习者。项目基于STM32F10x系列集成BSP驱动、RSA核心算法库含PKCS#1填充、应用层串口交互逻辑及Keil uVision工程配置解决了在无操作系统环境下移植大数运算密码算法的关键难题。压缩包共127个文件以46个.h头文件定义接口与结构体、43个.c源文件含硬件驱动、RSA加解密函数、主应用逻辑为主干辅以批处理清理脚本、Readme说明文档及固件库支持文件整体体积仅416KB轻量紧凑且目录模块清晰Bsp/rsa/app/CORE/USER等分层明确。目前已有20人学习下载读者可直接编译运行获取可验证的加密解密全流程代码、内存优化实践参考、UART数据交互范例及Keil调试配置模板是深入理解嵌入式密码学落地的重要实操样本。 最近整理嵌入式相关工程的时候翻出一个stm32_RSA.zip里面是当时给 STM32 做的一套 RSA 加解密验证工程。解压、编译、烧录、联调前后折腾了不少时间踩的坑也都还在。趁着记忆还热乎我把这个 zip 里到底装了什么、为什么要在单片机上跑 RSA、怎么用 mbedtls 落地、怎么和 PC 端的 node-forge 对上以及实测下来的性能数据和个人踩坑记录一次性整理清楚。如果你正打算在 STM32 上做固件签名验证、安全通信密钥协商或者在找一份能直接跑通的 RSA 例程这份记录可以直接当参考。很多人一听到“单片机跑 RSA”就觉得头大觉得这应该是服务器或者 PC 端才干的事。但实际上STM32 上跑 RSA 并不是什么稀罕操作尤其这几年设备联网、OTA 升级越来越普及固件被篡改、通信数据被劫持的问题也越来越多。RSA 在资源受限设备上的价值不在于加密大块数据而在于用少量字节完成密钥交换、签名验签这类安全动作。这个方向入门不难真正藏在后面的坑大概有这么几个一是 zip 工程包本身可能就有问题二是编译器/库的选型容易走弯路三是 PC 端和单片机端的密钥格式、填充方式不对上导致解密失败最后才是性能优化和安全性落地。下面按我实际整理的顺序把每个环节掰开来说。1. 这份 zip 包整理的思路STM32 上为什么要用 RSA先说说这个工程包的结构。stm32_RSA.zip解压之后核心其实很朴素一个 STM32 的裸机工程里面用 mbedtls 库实现了 RSA-1024 的加解密数据通过串口接收和发送同时配套了 PC 端 node-forge 的密钥生成与加解密验证脚本。因为当时是从零开始搞的工程里还顺带整理了标准库新建工程、串口收发、调试口配置、外设初始化这些基础代码等于把“从新建工程到跑通 RSA”这条链路都串起来了。为什么会在 STM32 上引入 RSA而不是直接 AES这是刚开始最容易困惑的问题。其实 RSA 和 AES 解决的是不同层面的问题AES 是对称加密加密解密用同一把密钥速度快但是密钥怎么安全地传递给对方是个难题RSA 是非对称加密公钥公开、私钥私有不需要提前共享密钥但是运算速度慢不适合做大流量数据加密。在嵌入式领域RSA 最典型的三个使用场景决定了它很难被替代固件签名验证生产环境里用私钥对固件做签名设备端用公钥验签。固件只要被改动验签必然失败设备拒绝启动。这种场景下数据量很小RSA 慢一点无所谓安全性却是刚需。通信密钥协商两个设备第一次通信时先通过 RSA 加密传递一个随机生成的 AES 密钥后续业务数据全部走 AES。这样既避免了对称密钥明文暴露又绕开了 RSA 速度慢的短板。一机一密/身份认证设备内置厂家签发的证书或密钥对服务端通过 RSA 验证设备身份。这种场景在很多物联网平台里已经是标配了。我整理这个 zip 的时候主要参考的就是第一类场景因为固件升级验签是绝大多数嵌入式产品最先遇到的安全需求。搞清楚这一点后面所有技术选型都会很明确不是要在 STM32 上实现一个“能用就行”的 RSA而是要把 RSA 集成进一个真实可维护的工程里让后续扩展 OTA、安全通信都有基础。顺带说一句当前热度很高的rsa加密算法和sm2也是个值得关注的方向。如果产品要过国密合规SM2 作为椭圆曲线体系的算法在签名速度、密钥长度上有天然优势STM32 上移植也基本是同一套路。但 RSA 仍然是理解公钥密码体系的敲门砖而且 mbedtls 把两者都支持了先跑通 RSA 再切 SM2 心理压力会小很多。2. 解压这一关zip 损坏、Linux 命令和工程环境的常见状况拿到stm32_RSA.zip的第一步当然是解压但这一步就埋了好几个雷。先说我自己碰到的从网盘下载完直接双击解压结果 7-Zip 弹了一个file is not a zip file的报错。当时第一反应是下载的文件坏了后来用十六进制编辑器看了一眼文件头才发现浏览器或者下载工具给文件加了个后缀真实文件其实是.rar或者是一个已经被杀毒软件处理过的副本。另一个高频报错是invalid zip archive: could not find eocd这个报错的意思是压缩包末尾缺少 End Of Central Directory 记录也就是 zip 文件的“目录结尾标志”找不到了。这里补一个背景zip 文件格式规定文件末尾 22 个字节左右必须有一个 EOCD 记录里面保存了注释长度、中央目录偏移量等信息。解压工具读取 zip 时首先找的就是这个记录。如果这个记录被截断、破坏或文件根本没有下载完整就会报could not find eocd。说白了绝大多数invalid zip archive的根因就是下载不完整或者上传源文件时就没生成正确的 zip 尾部记录。遇到这种情况推荐的做法是先用file命令识别真实文件类型不要在文件名上猜file stm32_RSA.zip假如输出显示Zip archive data说明文件头没问题如果显示HTML document那基本可以断定下载到了假页面。确认是 zip 但 EOCD 坏了可以尝试用 zip 自带的修复功能zip -FF damaged.zip --out repaired.zip unzip -t repaired.zipunzip -t是校验压缩包完整性最直接的方式能列出每个文件是否正常。如果修复不了老老实实重新下载不要浪费时间在残缺文件上。在 Linux 下解压这份工程我常用的一串命令是这样的unzip stm32_RSA.zip # 如果压缩包是用 Windows 中文环境压缩的目录名乱码时加 -O 指定编码 unzip -O GBK stm32_RSA.zip # 如果压缩包里的目录层级太深先看一眼再解压 unzip -l stm32_RSA.zipunzip -l看一眼目录结构是个好习惯尤其是拿到别人的工程包时提前了解文件组织方式能避免解压后一锅粥。反向操作打包时也有讲究zip -r stm32_RSA.zip stm32_RSA/注意-r参数必须带上否则只会打包空目录不会递归处理子文件。工程环境方面这个 zip 里同时包含了 Keil MDK 工程和 Linux 下 CMake 编译配置。为什么两份都要因为实际开发中会遇到两种情况如果你用stm32 linux开发环境用 arm-none-eabi-gcc 交叉编译配合 CMake 可以在 CI 流程里自动化构建如果只是开个板子调一调Keil MDK 还是最直接的。stm32 标准库新建工程最常见的错误是启动文件选错比如 F103ZE 和 F103C8T6 的启动文件确实不同但很多人复制工程时不改启动文件结果 Keil 编译能过一烧录就进 HardFault。另外还要注意标准库版本和core_cm3.h的对应关系串口、延时这些基础代码如果是从旧工程拷贝的必须先确认芯片型号一致否则外设寄存器地址都是错的编译也发现不了。还有一个小提醒涉及 zip 加密。压缩包如果用zip -P password这种方式加密安全性其实很弱因为 zip 传统的 ZipCrypto 算法是流密码已知明文攻击容易破。现在正规做法是用 7-Zip 的 AES-256 加密或者直接不用压缩密码改用安全渠道传输。至于网上常搜的zip密码移除这里多说一句如果密码忘了而压缩包不是你的那就不要把心思放在找破解工具上这类工具本身风险很高还可能被反打正确方式是联系压缩包的原始作者要密码或重新打包。这个原则在工程文件协作里特别重要。3. RSA 在单片机上不是玄学大数运算与 mbedtls 选型解压和工程环境的问题解决后真正进入核心算法部分。先花点篇幅把 RSA 的原理说清楚因为不搞懂原理后面调试解密失败时完全无从下手。RSA 的数学基础其实就三个知识点大素数分解、欧拉函数、模幂运算。简单说选取两个大素数 p 和 q计算 n p × q然后计算欧拉函数 φ(n) (p-1) × (q-1)。接着选择一个与 φ(n) 互质的数 e常用 65537再计算 d 满足 e × d ≡ 1 (mod φ(n))。加密时把明文 m 映射成一个小于 n 的整数计算 c m^e mod n解密时反过来计算 m c^d mod n。因为从 n 反推 p、q 极其困难别人即使拿到公钥 (n, e) 也无法算出私钥 d这就构成了安全性基础。注意上面说的“极其困难”是经典计算机视角下的结论这也是为什么现在很多新项目会考虑 SM2。SM2 基于椭圆曲线离散对数问题在同样安全强度下需要的密钥长度更短运算也更快。对于 STM32F103 这种主频不高的 MCU 来说SM2 确实比 RSA 更友好但 RSA 生态成熟、工具链完善学习阶段仍然非常适合先从 RSA 入手。这里的核心难点在于RSA 的明文和密文都是 1024 位或 2048 位的大整数而 C 语言里unsigned long long才 64 位。要在 MCU 上实现 1024 位的大数乘法、大数模幂还要保证速度自己从零写的话至少涉及大数表示、Karatsuba 乘法、蒙哥马利约减、滑动窗口幂模等一系列内容调试难度极高而且很容易出隐蔽的边界错误。因此我强烈不建议在 STM32 上手写大数运算直接用现成的密码学库。选型上mbedtls 是当时的第一选择理由有三点专为资源受限设备设计支持裁剪去掉不需要的模块后代码量和内存占用可以压得很低。无操作系统依赖裸机环境下直接用不要求文件系统、网络协议栈。提供标准 RSA 接口密钥导入、加解密、生成都有现成实现并且同时支持 PKCS#1 v1.5 和 OAEP 两种填充方式。把 mbedtls 加入 STM32 工程时最核心的是配置mbedtls_config.h。我这边用到的最小配置类似这样#define MBEDTLS_BIGNUM_C #define MBEDTLS_RSA_C #define MBEDTLS_PKCS1_V15 #define MBEDTLS_ENTROPY_C #define MBEDTLS_CTR_DRBG_C #define MBEDTLS_MD_C #define MBEDTLS_SHA256_C #define MBEDTLS_PLATFORM_C关键要理解每个宏的用途MBEDTLS_BIGNUM_C是大数运算基础RSA 必然依赖MBEDTLS_RSA_C是 RSA 算法本体MBEDTLS_PKCS1_V15对应的是 PKCS#1 v1.5 填充node-forge 默认也支持这种方式MBEDTLS_ENTROPY_C和MBEDTLS_CTR_DRBG_C用于产生随机数做加密填充时需要。如果工程中还不需要密钥生成功能可以不用全量启动熵源但加密接口mbedtls_rsa_pkcs1_encrypt在 v1.5 填充模式下仍然需要随机数所以随机数这一环不能省。选 1024 位还是 2048 位也是需要权衡的。1024 位在当前安全等级下已经接近淘汰仅适合学习验证真实产品强烈建议 2048 位。但要注意从 1024 到 2048单个私钥解密时间会长约 3 到 5 倍内存占用也会增加。为了让读者心里有数我给出在两块常见芯片上的实测数据Keil -O2 优化mbedtls 默认配置串口打印耗时芯片主频密钥长度公钥加密耗时私钥解密耗时RSA上下文内存约占用STM32F103C8T672 MHz1024 bit20 ms 左右260 ms 左右2.5 KB 左右STM32F103C8T672 MHz2048 bit80 ms 左右1.6 s 左右7 KB 左右STM32F407VET6168 MHz2048 bit35 ms 左右700 ms 左右7 KB 左右STM32F103C8T6 的内存只有 20 KB跑 2048 位时会明显紧张尤其是还要给串口缓冲、协议帧留空间。这也是为什么在低端芯片上做安全通信时很多人会先 RSA 协商密钥后续业务数据切到 AES而不是一直用 RSA。4. 从 node-forge 到开发板密钥联调与串口数据通路RSA 是典型的跨端密码系统单独在开发板内部自加密自解密没有实际意义。我这套做法是PC 端用 node-forge 生成密钥对公钥下发到 STM32STM32 用公钥加密一段数据把密文通过串口发回 PCPC 再用私钥解出来验证整条链路打通。反过来也可以PC 用公钥加密STM32 用私钥解密。node-forge 是一个纯粹用 JavaScript 实现密码学算法的库特别适合做这类联调工具不需要在 PC 上装重量级依赖。安装和生成密钥对很简单npm install node-forgeconst forge require(node-forge); // 生成 1024 位 RSA 密钥对 const keypair forge.pki.rsa.generateKeyPair(1024); // 导出 PEM方便查看和保存 const publicKeyPem forge.pki.publicKeyToPem(keypair.publicKey); const privateKeyPem forge.pki.privateKeyToPem(keypair.privateKey); console.log(publicKeyPem); console.log(privateKeyPem); // 用公钥加密一段短文本 const input hello stm32 rsa; const encrypted keypair.publicKey.encrypt(input, RSA-PKCS1-V1_5); console.log(Buffer.from(encrypted, binary).toString(hex));在 STM32 端我并没有选择把整个 PEM 文件塞进去因为 PEM 是 Base64 编码的 DER 结构解析起来在单片机上比较浪费。更直接的做法是把密钥的 N 和 E 两个参数提取出来转成十六进制字符串然后在 STM32 端用mbedtls_mpi_read_string导入。提取 N 和 E 的脚本const n keypair.publicKey.n.toString(16); // 大整数转十六进制字符串 const e keypair.publicKey.e.toString(16); // 通常是 010001 console.log(N:, n); console.log(E:, e);STM32 端加密接口的关键代码#include mbedtls/rsa.h #include mbedtls/entropy.h #include mbedtls/ctr_drbg.h static mbedtls_entropy_context entropy; static mbedtls_ctr_drbg_context ctr_drbg; static const char *PUB_KEY_N_HEX 你的N值十六进制字符串; static const char *PUB_KEY_E_HEX 010001; int rsa_public_encrypt(const uint8_t *input, uint32_t input_len, uint8_t *output, uint32_t *output_len) { int ret; mbedtls_rsa_context rsa; mbedtls_rsa_init(rsa); mbedtls_mpi_read_string(rsa.N, 16, PUB_KEY_N_HEX); mbedtls_mpi_read_string(rsa.E, 16, PUB_KEY_E_HEX); rsa.len mbedtls_mpi_size(rsa.N); ret mbedtls_rsa_pkcs1_encrypt(rsa, mbedtls_ctr_drbg_random, ctr_drbg, MBEDTLS_RSA_PUBLIC, input_len, input, output); if (ret 0) { *output_len rsa.len; } mbedtls_rsa_free(rsa); return ret; }这里有两个细节很容易忽略。第一rsa.len必须用mbedtls_mpi_size(rsa.N)初始化它决定密文长度。1024 位密钥时密文固定 128 字节2048 位时固定 256 字节。第二mbedtls 接口的输入输出缓冲区需要足够大不能只开一个几十字节的数组否则就是典型的数组越界隐患。在数据通路层面我是通过串口完成 PC 与开发板交互的。明文、密文都以十六进制字符串形式传输。STM32 端接收不定长数据时如果用的是 HAL 库优先考虑空闲中断加 DMA 的方式这也是stm32 hal库串口空闲中断热度居高不下的原因。硬件上串口接收用一个环形缓冲区DMA 负责把数据搬到内存空闲中断负责判断一帧数据收完了。这样既不用每一个字节都进一次中断也能适配可变长帧。如果只是临时测试也可以用逐字节中断加状态机的思路状态机里判断帧头、长度、数据、校验值但这种写法在波特率较高或者主频较低时容易出现丢字节的问题。我自己实际调通时用的帧格式很简单帧头0xAA 0x55接着 1 字节命令字2 字节长度N 字节数据1 字节累加和校验。这个格式虽然简单但足够稳定尤其在加了校验字节后能够把串口噪声造成的偶发错帧识别出来。这里必须提醒加了 RSA 之后由于密文看起来完全是随机的任何一 bit 翻转都会导致解密失败所以底层的传输校验很重要不能因为要省几个字节就省略。5. 实测数据与高频踩坑清单整个链路跑通后我把性能数据和踩坑记录都留在了 zip 里的 README 中。这些内容比代码本身更有价值因为很多问题不是看代码能看出来的。实测中我印象最深的数据是STM32F103 在 72 MHz 主频下1024 位 RSA 公钥加密约 20 ms私钥解密约 260 ms2048 位私钥解密超过 1.6 秒。在交互式联调时1.6 秒的等待还是有感知的但在固件升级验签场景里通常可以接受。另一个视角是栈与内存的占用。mbedtls 的 RSA 上下文和 mp 中间变量总内存需求不算小我在 STM32F103C8T6 上跑 2048 位时必须把全局缓冲区安排清楚否则连接器直接报 RAM 溢出。下面是踩坑清单按出现频率排序解密失败错误码是MBEDTLS_ERR_RSA_BAD_INPUT_DATA或MBEDTLS_ERR_RSA_INVALID_PADDING。这是新手最常见的错。绝大多数情况是两端填充方式不一致。比如 STM32 用 PKCS#1 v1.5 加密PC 端却用 OAEP 解密或者反过来。联调前必须先确认两端算法和填充方式完全对齐。node-forge 里加密时用RSA-PKCS1-V1_5就对上了。字节序问题。RSA 大数在串口传输时mbedtls 默认按大端字节序也就是高位在前。如果你把加密结果从 STM32 端通过串口工具发出来PC 端再转成 Buffer 时不小心做了小端解析解密必然失败。这个坑很隐蔽因为不加解密时看 hex 字符串根本看不出问题。栈溢出导致 HardFault。某些 STM32 工程默认栈大小只有 0x4001 KB而 mbedtls 的 RSA 操作在栈上会临时分配多个大数中间变量1 KB 远远不够。我把栈空间调大到 0x1000 之后系统才稳定下来。排查方法也很简单在 HardFault_Handler 里看栈指针或者用 Keil 的 Call Stack 窗口定位最后一次函数调用。CPU 主频和延时问题。如果你在工程里碰了stm32延时函数delay卡死多半是 SysTick 没有初始化或者中断服务函数没写。这个问题常见于从别的工程拷贝代码时SysTick_Init被漏掉了但主循环里调用了delay_ms。我建议所有例程里延时函数的初始化放在 main 函数最开始并且先烧一个点灯程序验证系统时钟正常再做 RSA 联调否则算法本身没问题也会被外设问题卡住半天。调试口被占用。stm32 禁用jtag这个话题在调试外设冲突时经常被搜索到。STM32 的某些引脚默认复用为 JTAG/SWD比如 PA15、PB3、PB4。如果你把这些引脚当作普通 GPIO 用需要调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)但这个操作会把调试功能关掉导致之后无法烧录和在线调试。如果发生这种情况最直接的办法是按住复位键不放点击下载在开始擦除的瞬间释放复位键让烧录器有机会连上芯片如果还不行只能通过 BOOT0 拉高进入 ISP 模式擦除。总之改这个配置之前先确认自己还有办法把程序擦回来。随机数熵源问题。mbedtls 的mbedtls_rsa_pkcs1_encrypt在填充时依赖随机数。很多 STM32 工程没有硬件随机数发生器于是就从系统滴答计时器取种子这在演示阶段问题不大但不同设备启动时间接近时种子可能相同加密结果也会相同安全性大打折扣。如果芯片支持硬件随机数比如 STM32F4 的 RNG优先用硬件随机数如果不支持至少结合 ADC 噪声、芯片唯一 ID 等多个不相关源来凑熵。在这些踩坑背后还有一个经验值得单独讲一下不要在同一块开发板上同时调多个串口外设或者 SPI、I2C 这类总线设备然后数据错乱时互相怀疑。加密数据本身有很高的抗干扰要求端到端联调之前最好先把原始串口环回测一遍。这个 zip 里还顺带整理了一些外设例程比如stm32 adc 多通道扫描循环采样dma、stm32 spi、stm32 做主机挂载u盘之类虽然和 RSA 不是同一领域但它们的调试方法和 RSA 联调是相通的先分层验证再整合测试信号层面的问题不要用应用层逻辑去猜。如果光耦隔离的串口通信出现乱码优先用示波器看波形边沿失真而不是去怀疑 RSA 代码写错了。6. 把这套代码用进真实产品安全启动与密钥保护跑通了例程接下来要考虑的是怎么把 RSA 用在产品里。这个部分如果做不好前面所有工作都只是 demo。真实产品和开发板的最大区别在于攻击者拿到了设备可以调试、读 Flash、探测引脚所有安全设计都必须默认攻击者拥有物理接触能力。典型的安全启动流程是这样的厂商在构建服务器上持有 RSA 私钥对固件做哈希后再用私钥签名签名值和固件一起发布设备端烧录时只保存 RSA 公钥每次启动或 OTA 时设备用公钥验证固件签名验证通过才允许跳转或写入。因为攻击者没有私钥即使篡改了固件也无法生成合法签名。具体到 STM32 实现这里的关键是公钥怎么存。公钥本身不保密但它必须防篡改。我的做法是把公钥的 N 值用二进制数组固化到程序里同时开启 STM32 的读保护RDP Level 1防止别人直接通过调试接口把 Flash 读出来替换公钥。如果对安全等级要求再高一点可以考虑把公钥哈希或公钥放到 OTP 区域配合 STM32 选项字节进行保护。stm32中flash很容易踩的几个坑包括写 Flash 前必须擦除扇区、擦除最小单位是扇区而非字节、写入地址必须按半字/字对齐、擦写过程中不能有中断或者至少要注意中断优先级。我在一次 OTA 升级测试中就是因为没有正确处理中断时序导致 Flash 写入校验失败直接变砖再重新烧录。另一个容易被忽略的问题不要把私钥放进 MCU。私钥一旦进了设备攻击者只要拿到 Flash 内容整个系统就彻底失守。正确做法是私钥只放在产线服务器或安全芯片里设备里只有公钥。如果你确实需要设备本地私钥做身份认证建议换用带安全密钥存储的芯片例如支持 TrustZone 的 STM32L5 系列或者外接 SE050 这类安全元素而不是裸奔在普通 Flash。数据通信场景里我推荐用“RSA AES”混合加密而不是纯 RSA。思路很简单通信双方先通过 RSA 加密交换一个 AES 密钥之后所有业务数据用 AES 加解密。这样既利用了非对称密钥管理的安全优势又规避了 RSA 大块数据加密的性能瓶颈。举例来说STM32 端先本地生成一个随机 AES 密钥用对端的 RSA 公钥加密后发送过去后续的所有传感器数据都用 AES-CTR 流式加密传输。这种模式在k210与stm32通讯这类异构设备协同场景里很常见K210 负责图像识别STM32 负责控制与通信两边的数据交互如果都是明文很容易被中间人监听或篡改先用 RSA 握手再切 AES代价小收益明显。最后再说一个容易掉进去的生产细节密钥轮换。很多设备出厂后固件和密钥永远不更新一旦泄露就是全平台沦陷。好的做法是在固件升级协议里支持密钥版本号和公钥更新机制定期轮换密钥对。这个工程量不大但对产品生命周期的安全提升非常关键。我当时第一次搭这套系统时为了省事把密钥写死在一个.h文件里结果安全测试发现只要拿到一个固件就能提取公钥并回放签名那次教训之后我才把密钥管理系统单独拆出来做。说到底RSA 在 STM32 上的应用门槛并不在算法本身而在于把密码学知识、嵌入式工程经验和安全设计意识整合到一条完整的链路上。从一个 zip 工程包开始把环境、算法、联调、性能、安全一层层走通之后再看 SM2、AES、安全启动这些东西都会轻松很多。从这个工程延伸出去我个人建议下一步做两件事一是把 mbedtls 里其他对称加密模块也裁剪编译进来做一个 RSA AES-GCM 的完整安全通信 demo二是自己写一个简单的 Bootloader 验签程序把固件签名验证跑通。这两件事做完你会明显感觉自己对嵌入式安全的理解上了一个台阶。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门