CoAP物联网协议加固:DTLS与OSCORE配置

CoAP 协议安全是物联网项目里最容易被跳过的一环:设备端跑通了 GET/POST,能上报数据就算验收通过,加密和身份认证被留到「下个版本」。等到传感器网络接进生产环境,UDP 5683 端口上的明文报文既能被任意伪造,也能被拿来做反射放大攻击。

这篇文章给出一条可落地的加固路径:用 DTLS 给 CoAP 套上通道加密,用 PSK 或 ECDSA 证书解决设备身份,再用 OSCORE 处理跨代理的端到端场景,最后补上端口收敛与放大攻击限速。全部命令基于 libcoap 4.3.x,可直接复制执行。

适用场景

CoAP 跑在 UDP 上,报文头只有 4 字节,专为低功耗广域网设计。以下环境必须做加固:

  • NB-IoT / LoRa 传感器接入:设备通过运营商网络直连云端 CoAP 服务器,链路上任何一跳都能看到明文温湿度、电表读数、门磁状态。
  • 工业现场网关:PLC 或 RTU 通过 CoAP 与边缘网关通信,一旦报文可伪造,攻击者能直接下发控制指令。
  • 智慧园区设备管理:摄像头、灯控、空调面板统一走 CoAP 发现与配置,/.well-known/core 暴露全部资源列表,等于把资产清单挂在公网上。

反过来,如果设备只在内网、链路已经由 WireGuard 之类隧道保护,再叠一层 DTLS 的收益有限,优先把端口收敛和访问控制做掉更实际。

前置条件

  • Linux 服务器一台,能编译 C 程序,已装 build-essential、libssl-dev、autoconf、libtool。
  • libcoap 4.3.4 及以上,编译时必须显式打开 DTLS,默认配置不带。
  • OpenSSL 3.x,用于签发 ECDSA 证书。P-256 曲线在低功耗设备上的握手开销比 RSA-2048 低一个数量级。
  • 设备侧固件支持 DTLS 1.2。存量设备若只支持明文 CoAP,先用网关做协议转换,把明文段限制在隔离 VLAN 内。
  • 已确认设备与服务器时间同步,证书校验依赖 notBefore/notAfter,时钟漂移超过有效期会直接握手失败。

原理说明

CoAP 报文结构与攻击面

CoAP 报文由 4 字节固定头、可变长 Token、Options 段和 Payload 组成。固定头里的 Type 字段区分 CON(需要确认)、NON(不需要确认)、ACK 和 RST。由于基于 UDP,没有 TCP 三次握手,源 IP 完全可以伪造,这正是反射放大的前提。

典型的放大手法是伪造受害者的源 IP,向大量 CoAP 服务器发送一个很小的 GET 请求(比如 /.well-known/core?rt=temp),服务器把体积大得多的资源列表回给受害者。单个请求的放大倍数通常在 10 倍以上,几百台暴露在公网的 CoAP 设备就能形成可观的流量。

DTLS 解决什么,OSCORE 解决什么

DTLS 1.2 是 TLS 的 UDP 版本,提供通道级加密与双向认证。它保护的是「设备到服务器」这一跳,报文经过中间代理时必须解密再加密,代理能看到明文。

OSCORE(RFC 8613)工作在 CoAP 层,把 Option 和 Payload 整体加密后放进 OSCORE 选项,中间代理只转发密文,无法读取也无法篡改。它解决的是端到端信任问题,代价是代理无法做内容级路由。

两者的选择很直接:链路可信、只需防窃听,上 DTLS;跨第三方代理、需要防中间人窥探,上 OSCORE。生产环境常见组合是 DTLS 保护第一跳、OSCORE 保护端到端。

PSK 与证书的取舍

PSK 模式握手快、算力开销小,适合电池供电的传感器,缺点是密钥分发和轮换麻烦,一台设备泄露就要全局换密钥。证书模式支持吊销和精细化身份,但握手需要 2 到 3 个往返,低功耗设备上耗电明显。建议按设备等级分层:A 类控制类设备用证书,B 类采集类设备用 PSK 并按批次分组。

操作步骤

步骤一:编译带 DTLS 的 libcoap

--with-openssl 和 --enable-dtls 必须同时给,只给前者编出来的 coap-server 仍然不接受 coaps://。编译完成后用 -h 确认帮助信息里出现证书相关参数。

sudo apt install -y build-essential libssl-dev autoconf libtool git

git clone --depth 1 --branch v4.3.4 https://github.com/obgm/libcoap.git
cd libcoap
./autogen.sh
./configure --with-openssl --enable-dtls --disable-doxygen --disable-manpages
make -j"$(nproc)"
sudo make install
sudo ldconfig

coap-server -h 2>&1 | grep -E "cert|key|PSK"

步骤二:签发 ECDSA 证书链

用一条 P-256 自签根证书签服务端证书,设备侧只需预置根证书。证书的 CN 必须与设备访问用的域名或 IP 一致,否则校验会失败,这一点与 HTTPS 完全相同。

mkdir -p /etc/coap/pki && cd /etc/coap/pki

openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -new -x509 -key ca.key -days 3650 \
  -subj "/CN=IoT-Root-CA/O=Example" -out ca.crt

openssl ecparam -name prime256v1 -genkey -noout -out srv.key
openssl req -new -key srv.key -subj "/CN=sensor-gw.example.com" -out srv.csr
openssl x509 -req -in srv.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -days 825 -sha256 -out srv.crt

chmod 600 ca.key srv.key
openssl x509 -in srv.crt -noout -subject -dates

步骤三:以 DTLS 模式启动服务端

监听 5684(CoAP over DTLS 的标准端口),并把 CA 证书传给 -C 参数,否则服务端不会校验客户端证书,等于单向认证。日志级别开到 7 便于观察握手过程。

sudo coap-server \
  -A 10.20.0.10 \
  -p 5684 \
  -c /etc/coap/pki/srv.crt \
  -j /etc/coap/pki/srv.key \
  -C /etc/coap/pki/ca.crt \
  -v 7

如果现场设备只支持预共享密钥,改用 PSK 模式启动,密钥用十六进制串表示。同一进程可以同时开 5683(明文)与 5684(加密)两个端口,但生产环境应当只保留后者。

sudo coap-server -A 10.20.0.10 -p 5684 \
  -k 00112233445566778899aabbccddeeff \
  -u sensor-batch-01 -v 7

步骤四:关闭明文端口并收敛访问来源

这一步比加密更关键。大量 CoAP 事故不是密钥被破,而是 5683 端口直接暴露在公网。用 nftables 把明文端口丢掉,并只允许已知网段访问 5684。

sudo nft add table inet coap_guard
sudo nft add chain inet coap_guard input \
  '{ type filter hook input priority 0; policy accept; }'

sudo nft add rule inet coap_guard input udp dport 5683 drop
sudo nft add rule inet coap_guard input ip saddr != 10.20.0.0/24 udp dport 5684 drop
sudo nft add rule inet coap_guard input udp dport 5684 ct state new limit rate 50/second accept

sudo nft list chain inet coap_guard input
sudo nft list ruleset > /etc/nftables.conf

步骤五:限制反射放大

即使只开放 DTLS 端口,握手包本身也能被当作放大素材。用 hashlimit 按源 IP 限速,把单 IP 的新建速率压在合理区间,同时限制单次响应体积,避免 /.well-known/core 被当作放大器。

sudo iptables -A INPUT -p udp --dport 5684 \
  -m hashlimit --hashlimit-name coap_dtls \
  --hashlimit-above 20/sec --hashlimit-burst 40 \
  --hashlimit-mode srcip -j DROP

sudo iptables -A INPUT -p udp --dport 5684 -m length --length 1200:65535 -j DROP
sudo iptables-save > /etc/iptables/rules.v4

步骤六:客户端接入与 OSCORE 叠加

先在运维机上验证证书链能走通,再批量给设备刷配置。OSCORE 用于跨代理场景,需要设备与服务端共享同一份主密钥(Master Secret)和发送者 ID,两个参数必须成对配置,只配一边会导致解密失败。

coap-client -m get \
  "coaps://sensor-gw.example.com:5684/.well-known/core" \
  -C /etc/coap/pki/ca.crt \
  -c /etc/coap/pki/client.crt \
  -j /etc/coap/pki/client.key

# 叠加 OSCORE 时额外指定安全上下文
coap-client -m get "coaps://sensor-gw.example.com:5684/sensors/temp" \
  -C /etc/coap/pki/ca.crt -c /etc/coap/pki/client.crt -j /etc/coap/pki/client.key \
  -E oscore-context.conf

配置验证

验证一:明文请求被拒绝

对 5683 发一个普通 CoAP GET,应当没有任何响应。注意 UDP 不会返回错误包,coap-client 会一直等到超时,这正是「端口已关闭」的预期表现。

coap-client -m get -B 5 "coap://sensor-gw.example.com/.well-known/core"
# 预期:超时无响应

验证二:DTLS 握手成功且加密生效

用 -v 7 观察握手日志,正常会看到 DTLS: session established 与协商出的密码套件。随后用 tcpdump 抓包确认 Payload 是密文,看不到资源路径。

coap-client -m get -v 7 "coaps://sensor-gw.example.com:5684/.well-known/core"

sudo tcpdump -i any -n -X udp port 5684 -c 20
# 预期:Application Data 段为不可读密文

验证三:错误证书被拒绝

用一张不在信任链里的证书连接,握手必须失败。这一步验证的是双向认证真的生效,而不是服务端无脑接受任何客户端。

openssl ecparam -name prime256v1 -genkey -noout -out fake.key
openssl req -new -x509 -key fake.key -days 30 -subj "/CN=fake" -out fake.crt

coap-client -m get "coaps://sensor-gw.example.com:5684/.well-known/core" \
  -C /etc/coap/pki/ca.crt -c fake.crt -j fake.key
# 预期:handshake failure / certificate verify failed

验证四:限速规则命中计数

确认 hashlimit 规则真的有流量经过,用 -v 看 pkts 计数是否增长。规则存在但从不命中,通常意味着链顺序不对或端口写错。

sudo iptables -L INPUT -v -n --line-numbers | grep -E "5684|hashlimit"
sudo nft list chain inet coap_guard input
ss -lunp | grep -E "5683|5684"

常见问题

Q1:coap-server 启动报 DTLS 相关错误,提示不支持 coaps

libcoap 默认编译不带 DTLS,很多发行版仓库里的包也是禁用的。用 coap-server -h 检查帮助里有没有证书与 PSK 参数,没有就说明编错了。必须从源码编译,且 ./configure 时同时给 --with-openssl --enable-dtls。

另一种情况是 OpenSSL 版本过旧,缺少 DTLS 1.2 需要的椭圆曲线支持。用 openssl version -a 确认是 3.x,并检查 openssl ecparam -list_curves | grep prime256v1 有输出。

Q2:DTLS 握手成功,但业务请求全部超时

先排除 MTU 问题。DTLS 握手包偏大,链路上如果有 PPPoE 或隧道封装,1472 字节的包会被丢弃,表现为握手停在 ClientHello 之后。把接口 MTU 调到 1400 再试,或者启用 DTLS 的分片支持。

其次是 Cookie 校验。服务端为了防止放大攻击会要求客户端回送 Cookie,一些精简实现的设备固件不支持这个扩展,握手会卡在 HelloVerifyRequest。要么升级固件,要么在服务端放宽 Cookie 要求,但后者会削弱防放大能力,不建议在公网环境使用。

Q3:设备算力太低,DTLS 握手把电量吃光了

三个方向可以缓解:把 prime256v1 换成更轻的曲线不现实,P-256 已是低功耗场景的下限;真正有效的是延长会话复用时间,用 session ticket 避免每次上报都完整握手;其次是减少心跳频率,CoAP 的 Observe 机制本身支持长连接推送,不需要设备轮询。

如果设备连 DTLS 都跑不动,退回「网关终结 DTLS + 内网明文」的架构,把明文段压缩在一个 VLAN 内,用交换机 ACL 挡住横向访问。这是妥协方案,必须配合资产梳理,不能当成长期状态。

总结

CoAP 加固不是装个证书就完事,它由四层构成,缺一层就有对应的攻击路径:

  • 端口收敛:关 5683,只留 5684,非必要不暴露公网。这一层拦住绝大多数自动化扫描。
  • 通道加密:DTLS 1.2 + ECDSA 证书,或按设备等级分组的 PSK。解决窃听与伪造。
  • 端到端保护:跨第三方代理时用 OSCORE,中间节点只看得到密文。
  • 抗放大:hashlimit 限速 + 响应体积上限 + 关闭 /.well-known/core 的公网可读,避免自己变成攻击跳板。

最后的提醒:把设备清单和密钥台账建起来,比任何单点配置都重要。见过太多现场,证书过期三个月没人发现,全站设备静默掉线;也见过 PSK 全批量用同一个值,一台设备被拆解就等于全网沦陷。加密做对了只是起点,密钥生命周期管理才是长期功课。