国标的安全要求分三层:认证(你是谁)、完整性(没被改)、加密(别人看不懂)。 摘要认证最妙的地方是「密码从头到尾不上线」;但它只保证这一点,不保证内容不被看。
上一节讲「多快算合格」,这一节讲「多安全算合格」。 这一节是国标对接里最容易被跳过、也最容易出事的一部分。 很多项目出事不是因为协议不懂,是因为设备还在用出厂口令。
一、认证:密码不上线
SIP 用的是摘要认证,不是明文口令: 平台下发一个随机数,设备拿它和密码一起算哈希,把哈希发回来。 平台那边存着同一个密码,自己算一遍比对,对得上就放行。整条链路上没有一处出现密码明文。 这样一个好处和一个坏处: 好处是抓包抓不到密码;坏处是平台必须存明文密码或它的等价物(HA1),不能只存不可逆摘要。 所以「密码泄露」这件事在国标里等价于「HA1 表泄露」——拿到 HA1 就能冒充设备注册。 这也是为什么 HA1 表要当密码一样保护。 随机数的作用是防重放:每次注册平台都下发新的随机数,同一份报文重发就对不上了。
二、完整性与防重放
摘要认证只保护「认证报文」,不保护「之后的报文」: 注册完之后点播、控制那些报文,抓包能看得一清二楚。 所以标准还要求对信令做完整性保护,常见做法是给每个报文加一个带时间戳的摘要, 或者干脆把整条链路套进 TLS。 媒体加密是可选项,实际用得很少:因为加密要消耗算力,而摄像头的算力很紧。 多数项目靠的是「网络隔离」而不是「媒体加密」。 这里要说一句实话:国标的安全能力是「够用」不是「很强」。 它设计于一个「设备在内网」的年代,放到公网上直接暴露,靠协议本身是挡不住的。
三、工程上真正的三道门
第一道门是网络:摄像机不该直接暴露在公网,该做的端口映射一个都别做,能走专线就走专线。 第二道门是口令:出厂口令必须改,这是所有安全事件里占比最高的一条。 不是「最好改」,是「必须改」。 第三道门是白名单:平台侧限制「哪些 IP 能注册」,这一条能挡掉绝大多数乱扫的尝试。 实现成本几乎为零,效果却比加加密好。 为什么把这三条放在协议之上:因为协议的安全机制要设备配合,而老设备往往不配合。 网络、口令、白名单这三样,不依赖设备实现,落地率最高。
四、动手 Demo:SIP 摘要认证,密码到底有没有上线
用哈希把摘要认证算一遍:HA1、HA2、response 各是什么,报文里发的是哪个。 再算「同一个密码、不同随机数」的三种情况,看防重放怎么起作用。
md5 = lambda s: hashlib.md5(s.encode("utf-8")).hexdigest()
ha1 = md5("%s:%s:%s" % (user, realm, pwd))
ha2 = md5("%s:%s" % (method, uri))
resp = md5("%s:%s:%s:%s:%s:%s"
% (ha1, nonce, nc, cnonce, qop, ha2))
print('Authorization: Digest username="%s",' % user)
print(' realm="%s", nonce="%s",' % (realm, nonce))
print(' uri="%s",' % uri)
print(' response="%s"' % resp)
for i, nn in enumerate((nonce, nonce, "b91d4e77c0a2"), 1):
print(i, md5("%s:%s:%08d:%s:%s:%s"
% (ha1, nn, i, cnonce, qop, ha2))[:16])
实跑输出:
--------------------------------------------------------------------
SIP 摘要认证,密码到底有没有上线
--------------------------------------------------------------------
**国标要求设备接入必须认证**,用的是 SIP 的摘要认证。**它最妙的地方是:密码从头到尾不上线。**
**这一节用哈希把这个过程算一遍。**
第一次注册,平台下发随机数 nonce
量 怎么算 前 16 位
------------------------------------------------------------------------------
**HA1** `MD5(用户:域:密码)` `8258341d7dd730ce`
**HA2** `MD5(方法:URI)` `393e36b0c4c33612`
**response** `MD5(HA1:nonce:nc:cnonce:qop:HA2)` `cf6dab95fe2d1ba5`
**报文里发出去的是 `response`,不是密码**:
Authorization: Digest username="34020000001320000001",
realm="3402000000", nonce="a7f3c1e05b2d",
uri="sip:3402000000@3402000000",
response="cf6dab95fe2d1ba5ca30e722bd4549c8"
**平台那边存着同一个密码,自己算一遍 `response` 比对**,**对得上就放行。**
**整条链路上没有一处出现密码明文。**
**nonce 的作用是防重放**:
次序 nonce nc response 前 16 位
------------------------------------------------------------------
**1** `a7f3c1e05b2d` `00000001` `cf6dab95fe2d1ba5`
**2** `a7f3c1e05b2d` `00000002` `fe40ab3340e09a8f`
**3** `b91d4e77c0a2` `00000003` `9236a138d325176a`
**第一行和第二行 nonce 相同、结果不同**,**因为 `nc` 递增了**;**第三行换了 nonce,结果也变。**
**攻击者截获第一条报文重发,`nc` 还是 1,平台一看「用过了」就丢。**
**但摘要认证只保证「密码不上线」,不保证「内容不被看」**:
**报文本身还是明文,抓包能看见设备编号、指令、甚至 SDP 里的地址。**
**所以标准还要求对信令做完整性保护和加密**,
**实际工程里常见的是「内网用摘要、跨公网套 VPN 或 TLS」。**
**最后量一下这件事的成本**:
HA1 算一次平均 1.68 微秒(20000 次实测)
一次注册要算三次,合计 5.03 微秒
**几微秒,完全可以忽略。**
**注意这里说的是「本机 Python 实现」**——**换台设备、换个语言,这个数会差好几倍**,
**但量级都在「微秒」这一档,不会成为瓶颈。**
报文里发出去的只有 response 那 32 个十六进制字符,
username 是公开的、nonce 是平台给的、uri 是请求目标,密码一个字节都没有出现。
这三行哈希的分工要记住:HA1 把「用户+域+密码」压成一个值,
HA2 把「方法+URI」压成一个值,response 再把两者和随机数混在一起。
第一行和第二行随机数相同、结果不同,
因为 nc 递增了——这是「同一个随机数下不能重放同一条报文」的机制。
第三行换了随机数,结果也变。
攻击者截获第一条重发,nc 还是 1,平台一看用过了就丢。
所以防重放靠的是两个东西一起:随机数换新、计数递增。
最后那个耗时是实测的,一次哈希一点几微秒,一次注册算三次也就五微秒,完全可以忽略。
但注意这是本机 Python 实现的数,换到嵌入式设备上会慢很多,量级仍在一毫秒以内。
30 秒实验
- 算一下:一次注册要算几次哈希?(三次:HA1、HA2、response)
- 想一下:为什么平台必须存密码或 HA1,不能只存不可逆摘要?
- 想一下:摘要认证防不住什么?(报文内容被抓包看见)
常见误区
- 以为「摘要认证等于加密」。 它只保证密码不上线,报文内容还是明文。
- 以为「设备支持国标就安全了」。 出厂口令不改,协议再规范也没用。
- 以为「媒体一定加密了」。 媒体加密是可选项,实际项目大多靠网络隔离。
自测三问(点开对答案)
1. 摘要认证为什么能让密码不上线? —— **因为发的是「密码参与的哈希值」而不是密码本身**,**平台用同一个密码算一遍比对即可。** 2. 防重放靠的是哪两样东西? —— **平台下发的随机数每次换新,加上客户端递增的计数器 `nc`**。 3. 工程上比加密更有效的三道门是什么? —— **网络隔离、改掉出厂口令、平台侧 IP 白名单**。下一节进流程:第一个流程是注册,它是所有国标交互的起点。
- ☐ (本节完,下一节 10.08.01 注册和注销)