安全设备工程-全-
安全设备工程(全)
原文:
zh.annas-archive.org/md5/9cc34deb91490b735f6c4c550b2ca9eb译者:飞龙
前言

互联网连接、数字商业模式、数据驱动服务、远程访问和数据分析——这些都为几乎每个行业带来了各种各样的需求和挑战。简单来说,大多数现代产品都需要某种类型的计算机集成在其中。更具体地说,它们通常需要一个嵌入式系统,这意味着一个包括处理单元、内存和输入/输出接口的电子系统,这些组件嵌入到更大的机械或电子系统中。
嵌入式系统的应用领域非常广泛。它们被用于工业自动化、交通运输和关键基础设施系统中的控制器、传感器和执行器中。路由器、交换机和基站等通信和网络硬件也以它们为基础。在消费市场中,典型的嵌入式系统产品包括智能洗衣机、智能取暖系统和游戏主机。即便是我们用来进行银行交易和门禁控制的塑料卡片,也可以视为一种嵌入式系统。
与个人电脑(PC)和服务器系统相比,这些设备往往面临一些限制,例如需要降低制造成本或运行在低到中等计算能力的硬件上,此外输入和输出能力也相对有限。嵌入式系统通常用于非常特定的领域,有时是关键领域,并且它们通常与用户交互较少,甚至没有交互。此外,这些设备由来自不同产品、制造商和行业的各种硬件、固件和操作系统构成。
除了这些限制之外,在方程中加入安全要求并不会让嵌入式系统工程师的工作变得更轻松。为这些设备开发安全措施、它们特定的应用环境以及受限的资源都使得架构师和开发人员面临挑战性任务。仿佛这些问题还不够,许多情况下,嵌入式系统还要面对物理攻击者,相比于远程访问云服务或网页服务等攻击模型,物理攻击是一种更强大的攻击模式。
嵌入式系统安全现状
如果我们看看不同的应用领域和行业,嵌入式系统的安全措施现状差异巨大。例如,智能卡解决方案在 1990 年代就曾面临欺诈案件。如果人们能够绕过加密和混淆算法,他们就可以免费观看付费电视。此外,如果攻击者成功克隆了这些智能卡,他们可以以更低的价格出售,从而导致原始服务提供商的收入损失。由于商业模式面临压力,这些公司的安全意识相对较高,并且优先投资和开发智能卡安全措施。
嵌入式系统在娱乐领域的另一个应用也展现了类似的模式:游戏主机。主机制造商的自然兴趣是只能在他们的设备上播放原版游戏媒体。如果攻击者成功运行了克隆的光盘,商业模式将遭受损害。随着逆向工程社区对游戏主机的分析兴趣增加,这例如促成了安德鲁·黄(Andrew Huang)在 2003 年出版的著名书籍《Hacking the Xbox》(《破解 Xbox》),行业也开始加强安全机制。因此,游戏主机在嵌入式系统安全方面达到了一个稳固的状态,攻击者需要投入大量的资源、专业知识和复杂的工具,才能成功绕过保护措施。
然而,在嵌入式系统的其他应用领域,组件并没有如此成熟的安全特性。2016 年,随着Mirai恶意软件的发现,这一点变得非常明显。该恶意软件利用了数十万个物联网(IoT)设备,主要是 IP 摄像头和家庭路由器,将它们转变为僵尸网络,执行大规模的分布式拒绝服务(DDoS)攻击,针对网站进行攻击。此外,2020 年被称为Ripple20和Amnesia:33的漏洞集合,暴露了嵌入式系统中 TCP/IP 协议栈的各种弱点。据估计,超过 1500 万个设备受到了影响,涵盖从医疗到建筑自动化,再到工业控制系统(ICS)的各个领域。
奇怪的是,用于工业自动化和关键基础设施的设备,尽管其稳健性和可靠性至关重要,也存在着长期的安全待办事项清单。尽管 2010 年的Stuxnet事件报告曾为工业自动化制造商敲响警钟,但 10 多年后的今天,市场上仍然存在大量未得到充分保护的设备。2022 年,一组针对运营技术(OT)组件的漏洞被以OT:ICEFALL的名称发布。作者将这些安全工程上的失败归咎于设计时不安全,因为被分析的产品甚至没有基本的安全控制。
新兴需求、法律与标准
听起来可能有些奇怪,但如果没有这些事件、漏洞和攻击,安全意识可能仅停留在边缘层面。然而,由于我们在过去 20 年里已经看到许多类似问题,同时在线连接性、数字服务和数据分析对公司变得越来越重要,网络安全“突然”成为了一项要求——例如,在采购过程中。
这并不意味着客户立刻展现出深刻且全面的安全知识,但他们越来越要求进行风险分析、采取保护措施,或满足产品制造商必须遵守的一系列(随机的)标准。从我在工业领域的经验来看,这有时会促使客户和制造商之间进行沟通,以在安全的实际需求和相关成本之间找到妥协,这对双方来说可能是一场合理且富有成效的讨论。
另一方面,政府越来越关注制定国家法律并签署国际协议,推动每个市场上销售的产品应当满足基本的安全要求。在欧洲,2019 年的《网络安全法案》(CSA)旨在为所有在欧盟销售的产品和服务建立安全认证框架,而 2024 年的《网络韧性法案》(CRA)则规范了具有数字元素的产品的网络安全要求。欧洲标准 ETSI EN 303 645 已定义了基准安全要求,特别是针对消费类物联网产品。在大西洋彼岸,拜登于 2021 年 5 月发布的第 14028 号行政命令采取了类似的政策,旨在提升物联网设备和软件解决方案的网络安全性。美国国家标准与技术研究院(NIST)已经提供了关于这些产品的网络安全标签的建议。
与此同时,多个行业的联盟正在尝试就各自领域的共同安全标准达成一致。一个显著的例子是国际电工委员会(IEC)标准 62443,专注于工业控制系统(ICS)安全和工业物联网(IIoT)。该标准结合了操作员、系统集成商和组件制造商的安全要求,提供了对工业系统统一且相互关联的安全视角。关于安全设备工程,IEC 62443 的第 4-1 部分和第 4-2 部分最为相关:第 4-1 部分涵盖了安全开发过程的实践,第 4-2 部分则涉及技术产品安全要求。
谁应该阅读本书?
如果你是参与客户讨论的嵌入式系统架构师,如前所述,本书将为你提供必要的知识,帮助你与合作伙伴进行平等的辩论。
如果你是负责实施安全功能的嵌入式系统工程师或物联网开发人员,并且想了解这些功能背后的逻辑及典型障碍,本书将为你准备好迎接未来的挑战。
如果你参与产品需求工程过程,或者在日常工作中进行嵌入式系统测试,本书将帮助你理解某些安全功能的价值,以及为什么开发团队的同事可能不愿意实现这些功能。
如果你是学生,想知道为什么许多保护措施在 IoT 产品中不能理所当然地采取,本书将证实你有正确的心态,并让你明白嵌入式系统安全是一项重要但有时繁琐的任务。
如果几分钟前有人对你大喊,“我们需要立即实现这个该死的设备安全!现在!!!”,深呼吸,取消所有预约,认真阅读本书。之后,你就准备好进行一次友好、客观的“安全”讨论。
本书涵盖了哪些内容?
本书的内容基于我在过去 15 年中在嵌入式系统安全领域的实践经验和研究洞察。
在第一部分:基础知识中,你将学习与提供安全开发生命周期相关的基础知识,以及如何使用加密技术。
第一章:安全开发过程 涵盖了在产品开发过程中遵循设计安全原则所需的基本要素。
第二章:加密技术 总结了与实际安全工程相关的加密技术要点。
第二部分:设备安全构建模块 详细介绍了嵌入式系统安全的基本物理和逻辑构建模块。
第三章:随机数生成器 深入探讨了随机性的神奇领域,强调其在安全中的重要性,并提供了生成和评估随机数据的实用建议。
第四章:加密实现 讨论了加密算法的实现选项及其对性能等属性的影响。
第五章:机密数据存储与安全内存 专注于以安全、机密的方式存储小型和大型数据。
第六章:安全设备身份 关注嵌入式系统的唯一身份的生成与管理。
第七章:安全通信 展示了用于通信渠道的先进保护措施,并回答了有关在嵌入式系统中实现这些措施的常见问题。
第三部分:先进的设备安全概念 专注于与安全 IoT 设备相关的全面保护概念。
第八章:安全启动与系统完整性 涵盖了嵌入式系统启动过程中,敏感操作阶段的安全考虑。
第九章:安全固件更新 描述了在考虑安全的情况下,提供产品软件更新的复杂性。
第十章:强固的设备架构 讨论了在受到攻击时如何继续运行关键进程的问题。
第十一章:访问控制与管理 考虑了设备上用户和进程的限制及其实际后果。
第十二章:系统监控 通过探索使你能够检测和分析嵌入式系统中的异常或攻击的措施,完成本书的内容。
阅读本书时,请记住,你的目标不仅是吸收尽可能多的技术知识,还要理解何时以及为什么设备安全措施是有意义的。
关于本书中的案例研究
本书中的几个章节包含了实际的案例研究。它们的目的不是作为可以轻松复制粘贴到自己设备上的示例,甚至不能用于生产性开发。那需要更高层次的细节,超出了本书的范围,而且相关的安全见解会在实现问题的海洋中消失。
一些案例研究展示了理论与混乱的现实世界之间的差距。另一些则展示了不同实现选项的优缺点,还有一些案例研究提供了一个特定的应用背景,帮助你理解前面提到的思想和概念。
如前所述,嵌入式系统是一类多样的设备,它们的处理器、内存和接口也各不相同。为了提供一个合理的演示设备——既不高端,也不太小巧和受限——我选择了一个中等性能的硬件平台,它还包括可以在实际案例研究中分析的基于硬件的安全措施:来自意法半导体(STMicroelectronics,简称 ST)的 STM32MP157F-DK2 评估板。(顺便提一下,我与 ST 没有任何关联。)
当谈到中高性能嵌入式系统的操作系统时,Linux 是自然的选择。它广泛应用于汽车、洗碗机、可编程逻辑控制器(PLC)、电视、能源监控系统,也在本书中的案例研究中得到了使用。具体来说,我使用了 ST 的 OpenSTLinux 发行版,配备 Linux 5.15 内核。
第一部分:基础知识
我曾经认识过一些在行业中非常出色的嵌入式系统工程师,他们热衷于最低级别的硬件开发,能背诵他们工具链中的每一条脚本,并且能够从微控制器的固件中挤出最后一点性能。然而,当谈到嵌入式系统的安全性时,我常常发现他们在两个基本领域存在知识不足。
首先,许多人并不熟悉建立安全开发过程所需的基本方法和组织措施。其次,即使他们知道一些加密算法的名字,也未必完全理解它们的属性、参数和限制,以及背后的推理。考虑到这一点,本书的第一部分将描述开发安全产品所需的基础概念。
第一章:安全开发过程**

当我还是学生时,我认为组织过程是工程学中最枯燥的课题之一。然而,在从事安全工程工作十多年并帮助组织优化安全工作后,我不得不承认,过程比我想象的更有趣,且在开发安全产品时完全不无关紧要。
尽管一个产品的技术保护特性可以完成并标记为完成,但安全开发过程永远不会完成;它必须不断维护和改进,这也是为什么安全工程过程的定性衡量标准被称为成熟度。它可以上升或下降,取决于支持在组织内开发安全产品的活动的规律性、质量和组织结构。
本章的目标是说服学生、工程师和开发者,过程不仅仅是创建没人阅读的文档。安全开发生命周期(SDL)关乎文化和日常行为;它关乎在变化的条件下维持质量和安全,并且阐明了如何让每个参与产品工程过程的人都能持续贡献于安全的最终产品。
关于各种指南
安全开发过程的建议已经存在了一段时间。然而,根据你的行业情况,可能到目前为止你所在的组织还没有考虑过采用这样的系统化方法,或者至少没有明确提出过。
开发操作系统和网页应用程序的公司较早就面临了安全问题,因此微软和开放网页应用安全项目(OWASP)是最早着手研究并讨论 SDL(安全开发生命周期)的公司之一也就不足为奇了。自国际电工委员会(IEC)发布 IEC 62443 第 4-1 部分以来,越来越多用于生产设施、关键基础设施和各种自动化应用的工业部件制造商开始意识到实施安全开发过程的必要性。
微软的 SDL 遵循 12 项实践(www.microsoft.com/en-us/securityengineering/sdl/practices),这些实践描述了公司为开发安全(软件)产品所采用的过程。这些过程包括从培训到需求工程、威胁建模、安全测试等。此外,OWASP 也面向软件社区,使用五个类别总结其软件保障成熟度模型(SAMM,owaspsamm.org/model/):治理、设计、实施、验证和运营。IEC 62443 的第 4-1 部分(*webstore.iec.ch/publication/33615)旨在为工业组件制造商建立一个安全开发过程。它分为 8 项实践,每项实践包含 2 到 13 个子实践,详细列出了从安全管理到设计与实施,再到现场更新管理等推荐活动。
这三条指南使用不同的结构来解释安全开发过程的理念;然而,你可能会注意到它们之间有显著的重叠。例如,微软 SDL 告诉你执行威胁建模,而 OWASP 在其设计类别中使用了威胁评估一词,IEC 62443-4-1 的要求 SR-2(它是安全需求规范实践的一部分)解释了为产品建立并维护威胁模型过程的必要性。
此外,安全知识和能力的重要性在这三者中都有体现:微软将提供培训列为其首要实践,而 OWASP 将教育视为治理的一部分,IEC 62443-4-1 在其安全管理实践的要求 SM-4 中使用了安全专业知识一词,来总结技能识别与发展的过程。作为最终示例,渗透测试在微软中是明确列出的实践;它在 IEC 62443-4-1 的安全验证与验证测试实践中的要求 SVV-4 中以相同名称出现,而 SAMM 在验证项目下的安全测试任务中提到了它。
我们可以继续讨论其他主题,如使用最先进的加密技术、澄清第三方依赖关系和漏洞管理等。重点是,在内容方面,这些指南有许多相似之处,因此对于“在众多可用资源中,哪个是最好的?”这个问题的回答是:“其实并没有太大区别。”
微软在安全软件工程方面拥有超过 30 年的经验。OWASP 不仅提供了有价值的建议,还提供了开源工具来支持这些过程。另一方面,IEC 62443-4-1 针对的是工业领域和组件制造商,这与传统的 IT 公司有所不同。此外,独立机构可以认证是否符合 IEC 62443 标准,但微软和 OWASP 的 SDL(安全开发生命周期)无法进行认证,这可能是一个决定性的因素。
这三份文件只是常见的示例。NIST 的《特别出版物 800-160》(csrc.nist.gov/pubs/sp/800/160/v1/r1/final?)、Building Security In 成熟度模型(BSIMM,* www.bsimm.com )倡议,以及欧盟网络安全局(ENISA)《物联网安全的最佳实践——安全软件开发生命周期》(www.enisa.europa.eu/publications/good-practices-for-security-of-iot-1 *)也非常有帮助。
产品安全责任
每个产品的安全成功始于职责的明确划分。无论是初创公司的创始人、全球公司的产品安全官(PSO),还是开发团队中的成员承担责任,都不重要,重要的是某个人要负责,并且这个人拥有足够的时间和资源来完成这项工作。
安全开发过程由一组异构的活动组成,这也意味着负责安全的人必须参与许多产品开发阶段。这个人必须确保从设计开始就考虑安全成为开发文化的一部分。当然,这并不是最有吸引力的角色,尤其是当安全在你的组织中之前并未受到关注时。尽管(产品)管理层通常喜欢谈论新技术和设备功能的潜力,但经常强调其中涉及的风险并不会让你成为每个人的宠儿。
然而,从长远来看,这个人将消除错误、漏洞和内部误解的源头。他们不仅会提高设备安全性,还会促进产品开发团队与管理层之间的透明度。
实践中:安全工程专家
我曾作为顾问为一家中型公司引入安全性到其产品工程流程中。当时,这家公司在 IT 部门有一位安全专家,但嵌入式系统工程团队中没有具备安全知识的人。
幸运的是,公司聘请了一位充满激情的年轻专业人士,他具备扎实的安全专业知识,甚至有设备工程经验,来填补这个空白,并负责产品安全。怀着一些关于如何建立安全开发流程的想法,他开始熟悉公司的具体流程、人员和产品。
他时常被要求在软件和硬件工程项目中支持开发团队,因为项目的紧迫性。可悲的是,这类任务的数量逐渐增加,而管理层对产品安全的关注却显著下降。大约两年后,他辞职了。而这家公司因此损失了两次:它失去了忠诚的安全人才,同时也失去了在竞争对手之前建立安全开发流程的机会。
意识与培训
几年前,我参加了一场德国安全顾问的讲座,内容讲述了欧洲公司如何为即将到来的网络威胁做好准备(或没有准备)以及安全领域的从业人员每天所经历的压力和挫折。演讲面向管理层代表,最后他总结道:“不是工具也不是技术——人才创造安全!”
从需求工程到软件和硬件开发,再到漏洞处理,正是人们创造性地发现了如何滥用产品功能,严格遵循团队内达成的安全编码规则,并分辨出第三方报告的相关和不相关的安全问题。
换句话说,如果你的开发团队无法想象潜在的攻击场景;将安全实践视为障碍;并以愤怒、恐惧或完全不理解的态度回应漏洞报告,那么即使你购买了最昂贵的顶级工具许可来“支持”它们,你的产品在安全性方面也不会有所提升。显而易见的问题是,“谁需要具备什么样的安全知识,才能在每天的工作中遵循安全设计原则?”
对于大多数员工来说,通用的安全意识是基本要求。这一要求对于工程师、开发人员和设备架构师同样适用。然而,安全意识并不是你在学校学到的东西(至少目前是这样)。它也不是大学教育或典型的在职培训计划中的强制性部分。此外,意识常常随着时间的推移而消失,这意味着它必须定期刷新。在你的团队中,必须解决并讨论攻击者的视角、他们的意图,以及应对这些策略的可能性。
根据我的经验,基于网络的培训通常是员工进行的“点击即忘”式活动,而社交聚会、研讨会和团队建设活动则能够促使互动、讨论,最理想的是能够进行反思,这使得人们能够将安全知识与日常工作相结合。
除了安全基础知识外,产品工程团队的成员可能还需要特定的能力。例如,开发人员可以从安全编码培训中受益,而测试人员可以通过参加渗透测试课程来提升技能。尤其在中型公司中,参与安全工程社区常常被忽视,但这可能是一个值得尝试的途径,可以与其他组织的管理层和技术人员交流想法、挑战和经验教训。
再次强调,培训并非一次性活动。持续地推动员工反思他们工作和日常流程的安全性,是建立生动的安全文化的一个重要组成部分。
资产和保护目标
想象一个阳光明媚的星期一早晨。接下来的这一周似乎像往常一样繁忙,但突然管理层有人问你是否能抽出一分钟:
经理:“请把我们的产品做成一个安全的产品!”
你:“你到底是什么意思?”
经理:“黑掉它应该是不可能的!”
你:“这个需求是从哪里来的?”
经理:“你没看新闻吗?现在什么都在被黑!”
你:“好吧,哪部分产品值得被黑?”
经理:“我不知道;你告诉我!”
这个对话是虚构的,但你可能有过类似的讨论。它明确了一个简单的观点:如果你不知道要保护什么,就无法保护设备。本质上,你公司里的每个人都应该知道什么是值得保护的,或者什么是对保持业务运行至关重要的。我们通常称之为资产。
有价值的产品部分
资产可以是各种类型的——例如,一个在按需支付场景中必需的加密密钥,一个从设备传输到云应用的测量数据,以启用预测性维护服务,一个激活或禁用产品功能的配置文件,一个在紧急情况下发出警报的固件代码,或者一个基本设备操作所需的 Web 服务。简而言之,资产就是任何在某种程度上重要和有价值的东西。
确定这些资产是启动安全开发过程的初步分析,也是你所有保护工作基础。逻辑上,你应该在实施对策或进行渗透测试之前确定你的资产是什么,因为如果你不知道要保护什么,又怎么能实施有效的对策并指定最坏的攻击场景呢?然而,由于大多数现代设备不是独立存在的,这一过程还涉及在组织内部建立共同的系统理解,这包括相关的对象、数据流、人员、第三方组件、生命周期过程以及背后的商业模型。
阅读这个列表时,你可能会觉得它涵盖了大量信息,你是对的。话题涉及从商业洞察到深度技术知识,再到流程和组织结构。无论你的公司大小,单个人通常不会负责所有这些任务,尤其是在开发团队内,因此以多维度的方式来进行分析是至关重要的。
注意
我在设立一个由主持人、安全专家和一群参与特定设备业务运营的员工组成的工作坊时,曾有过很好的经验。除了开发人员、测试人员和产品经理外,邀请来自维护、客户支持、数字服务、设备安全、销售以及任何可能与产品相关的部门的人员也是有意义的。
与团队成员讨论网络安全的起点是一个收集架构信息的基本概述。图 1-1 显示了一个虚构的 Wi-Fi 控制工业通风机的此类表示示例。

图 1-1:基本设备上下文概述的示例
这个初步概述应该简单,但足够详细。之后,在工作坊或讨论中的一个有时可能很混乱的过程中,这个概述将通过信息的补充和完善得到丰富,可能包括原始文档中没有明显的数据流、以前未知道的与第三方的关系,或者仅由那些每天执行它们的人知道的隐性过程步骤。
在每个人都对整体系统表示的细节和正确性感到满意后,实际的资产识别从这个问题开始:“系统中的哪些部分对我们的商业模式和/或我们的组织至关重要?”
相关保护需求
确定不仅是什么需要保护,而且是为什么它重要,这直接引导我们定义保护目标。
每个资产应该至少有一个保护目标;否则,从安全角度来看,资产可能不相关。以下列表,从著名的 C-I-A 三元组(机密性、完整性、可用性)开始,列出了定义哪些资产属性值得保护的典型保护目标。然而,如果你识别到一个不符合此列表标准目标的特定保护需求,将其用自己的话写下来也是没有问题的。
保密性 (C) 这一保护目标可能是最广为人知的。我们可以说,相应的资产必须保持“机密”才能“安全”。更正式地说,这意味着对特定资产的读取访问应该只有授权实体可以进行。尽管这个目标在谈到安全时可能是每个外行人首先想到的,但它并不总是正确的,并且并非所有资产都需要此保护。典型的需要保密性的资产包括存储的加密密钥或包含知识产权的可执行文件。
完整性 (I) 只有授权主体应该能够更改特定对象或数据,如果其他人进行修改,应该能够检测到这种篡改。常见的应用场景包括用于计费的单调计数器,或通过通信通道发送的控制命令。此外,您可以将此目标应用于整个设备或机器,以强调系统完整性的重要性,例如,如果能够替换系统的部分组件。请注意,完整性一词在安全领域的含义与安全领域不同,在安全领域,循环冗余检查(CRC)通常被认为是“完整性保护”。但在安全领域,这一概念并不成立,因为一个主动攻击者可以轻松伪造 CRC。
可用性 (A) 虽然设备的大部分部分应可用以确保其正常运行,但此保护目标用于数据或服务(无论是在设备上还是在后端),如果攻击者可能中断或延迟访问它们。相应资产的例子包括为设备提供实时数据的 Web 服务、设备主处理单元内的随机数流,甚至服务器系统上的备份。特别是在汽车和工业应用场景中,在硬实时通信的上下文中也存在类似的要求。在这种情况下,当前信息(例如来自安全传感器或刹车控制器的信息)必须在给定的截止时间之前可用。
真实性 (Au) 我们可以将这个保护目标视为完整性的扩展。除了确保数据没有被篡改外,这一目标还要求我们能够明确验证数据来自特定身份。真实性特别相关,如果您的设备的一部分必须是“原厂的”,例如备件、存储在闪存中的固件或许可证文件。
不可否认性 (Nr) 与真实性密切相关,不可否认性意味着实体不能合理地否认执行过某个操作。通常,这一要求对于导致合同产生的设备和/或用户活动非常有用——例如,直接在设备上订购打印机的墨水补充,或者如果您的产品包括按次付费的功能。
隐私(Pr)、假名化(Ps)和匿名性(An) 自 2018 年欧洲通用数据保护条例(GDPR)出台以来,隐私和数据保护问题受到越来越多关注。如果你的设备处理个人数据(例如,医疗设备),你可能还需要为这些数据指定保护目标。典型的目标可能是假名化,即数据可以追踪到一个化名,而不是一个真实的人,或者是匿名性,即处理或收集的数据既无法与真实的人关联,也无法与化名关联。
最后,你可能会得到一个资产及其对应保护目标的表格,如表格 1-1 所示。请注意,“备注”列可能会对你想要提供更多背景信息的保护目标非常有用。
表格 1-1: 示例资产及其对应的保护目标
| ID | 资产 | 保护目标 | 备注 |
|---|---|---|---|
| AS01 | 固件 | I, Au | 仅限原版固件 |
| AS02 | 证书私钥 | C | 如果泄露,设备可能被克隆 |
| AS03 | 温度传感器数据 | I, Au, A | 对检测设备滥用至关重要 |
| AS04 | 调试接口 | C | 仅限内部技术人员访问 |
| AS05 | . . . | . . . | . . . |
在处理了如此大量的信息后,人们通常会对设备安全与产品开发及运营各个领域的交织感到震惊。然而,他们通常会感到满意,因为他们已经迈出了朝着安全设备发展的第一步,并且了解到一些他们曾认为自己从 A 到 Z 都了解的产品的新知识。
实践中的威胁建模工作坊
几年前,我受邀在一家工业控制系统制造商那里进行为期两天的风险分析工作坊。根据我的建议,公司邀请了来自不同领域的几位人员参与。
在工作坊的前几个小时,一位开发人员正在解释设备中如何实现关键工业网络与 IT 网络的分离,这时一位来自技术维护的人员插话说:“我们在外面不是这么做的。”他接着解释说,在维护过程中,将这两个网络桥接起来要方便得多,而且这种桥接连接有时会在设备运行期间保持存在。
这一刻引发了许多讨论,促成了一个主要识别风险,最终导致了设备附加网络监控功能的开发。随后,维护说明手册也考虑到安全性进行了更新。这个问题之所以出现,完全是因为一个异质的跨学科小组在讨论产品安全时提出了不同的观点。
攻击者、威胁与风险
在建立了一个共同的系统来理解和识别你设备的资产和环境之后,便可以开始分析针对你特定使用案例的威胁和风险。第二阶段的目标是开发一套能够代表你当前威胁环境的场景,并根据它们发生的概率和对你的业务和组织的潜在影响进行评估。
威胁场景 通常由威胁行为者(进行攻击的人)、可能的漏洞(你设备的可利用弱点)、攻击路径(攻击使用的路径或方法)以及对你早先识别的至少一个资产的影响组成。这个阶段试图预测你的设备将来可能面临的威胁,即使它还没有开发或生产出来。
当然,你无法以百分之百的准确性全面实现这种预测。这是一个创意过程,需要参与者扮演攻击者的角色,并设想如何实现他们的恶意目标。是的,开发威胁场景对于技术人员来说可能有些不太结构化,可能会出现惊喜,结果也不是一成不变的。如果发现新的威胁,你还需要在未来更新你的场景。
潜在对手
一个好的起点通常是想象潜在的攻击者,他们在动机、机会和资源方面各不相同。以下可以作为基本的示例:
脚本小子 这些攻击者不一定是青少年,但他们是出于好奇心和娱乐目的分析设备和服务的人。他们通常资源有限(除了闲暇时间),使用互联网上的标准工具,并通过观看视频来获取基本技能。
安全研究人员 这些人拥有专业的知识和强大的设备,通常在大学里。他们虽然通常没有财务利益,但他们的目标是发布他们发现的漏洞并准备相应的修复方案。与其他攻击者类型不同,他们通常愿意与公司合作,以提高产品安全性。
网络犯罪分子 这些有组织的犯罪分子每天都在攻击 IT 产品,驱动他们的动力是财务利益。无论是勒索软件、拒绝服务(DoS)攻击,还是个人数据的销售,这些攻击者至少有一定的资源,积极监控行业,并能将他们的工具适应新场景。
产品盗版者 克隆设备是嵌入式系统面临的一个主要问题。这些攻击者装备齐全,可以复制硬件设计和软件组件以供自己使用。他们利用原始设备制造商的开发投资,提供价格较低的假冒产品。
国家级攻击者 —— 这些攻击者拥有复杂的设备、几乎无尽的资源和独特的情报数据访问权限,仅仅是其中的一些特征。他们通常出于政治或军事动机行动,因此他们通常超出了标准商业解决方案的范围。然而,尤其是对于在联邦或关键基础设施中运行的设备,考虑到此类对手是很重要的。
这些广泛且通用的攻击者类型有助于识别常见风险,但通常设备及其应用程序会有更具体的对手,这有助于让威胁场景更加具体,并帮助你识别组织面临的个别风险。例如,在汽车和摩托车领域,发动机调校可能是某个特定攻击者群体的目标,他们旨在绕过保护措施以应用自己的定制性能设置。
或许连客户本身也可能对操控他们的设备感兴趣。以农业物联网系统为例,环境保护和动物保护活动家可能是具有自己目标、资源和手段的现实攻击者。
最后,不要忘记内部人员作为攻击者,因为他们对内部文件、数据和流程有独特的知识和访问权限。当然,大多数员工可能是值得信任的,但思考一下如果同事有不良意图,他们可能做出什么行为,对于这一阶段是很有价值的。表 1-2 显示了表 1-1 中列出的资产的攻击者示例及其对应的属性。
表 1-2: 可能攻击者及其属性示例
| ID | 攻击者 | 动机 | 资源 |
|---|---|---|---|
| AT01 | 网络犯罪分子 | 财务 | 中等,恶意软件访问 |
| AT02 | 脚本小子 | 娱乐 | 低,互联网工具,闲暇时间 |
| AT03 | 环保活动家 | 宣传 | 中等,公关联系人 |
| AT04 | 维护人员内部人员 | 财务/愤怒 | 技术文件、设备账户、源代码读取权限 |
| AT05 | 客户 | 财务 | 日常设备使用,实验时间 |
| AT06 | . . . | . . . | . . . |
在编制这份清单时,人们有时会回想起过去发生的盗窃、破坏或海盗行为。相关信息对优化未来的保护工作非常有帮助。
潜在的负面影响
在一方面,我们拥有对业务至关重要的资产,而另一方面,我们有待机会进行攻击的嫌疑人。接下来合乎逻辑的问题是:“这些潜在的恶意行为者如何影响我们的资产?”
构建攻击向量并想象系统中可能存在的漏洞,可能会导致大量重叠的威胁场景,增加处理工作量,而且仍然无法涵盖所有内容。解决这一问题的一种方法是采用实用的方式,列出资产并应用由微软开发的STRIDE 方法论所建议的相关威胁。
表格 1-3 通过展示 STRIDE 缩写的含义来解释这一方法论——即五种标准威胁——并参考它们旨在打破的目标,以及主持人可以向所有参与者提出的样本问题。对于表格 1-1 中的 AS01——一个承载完整性和真实性保护目标的固件镜像——任务是创造性地思考篡改或伪造镜像原始制造商身份的方式。
表格 1-3: STRIDE 威胁清单
| Threat | Target | Question to pose |
|---|---|---|
| Spoofing | 真实性 | 有没有办法冒充任何实体? |
| Tampering | 完整性 | 有没有办法篡改任何数据? |
| Repudiation | 不可否认性 | 有没有办法合理地否认一个行为? |
| Information disclosure | 保密性 | 有没有办法读取/提取信息? |
| Denial of service | 可用性 | 有没有办法中断/延迟服务? |
| Elevation of privilege | 授权 | 有没有办法在未经许可的情况下行动? |
如果其他任何 STRIDE 威胁似乎适用,将其添加到讨论中。例如,在固件案例中,您可能有之前未考虑过的机密知识产权。最后,将任何额外的威胁与您的攻击者列表匹配,并讨论谁将是该威胁的合适行动者。
除了设备和软件共同面临的威胁外,物理产品还面临另一个问题,这应当是大家在头脑风暴威胁时的备忘事项:它们可以被物理攻击。在某些情况下,这可能不是关键问题,因为如果人们能接触到设备,就可以破坏设备控制的机器或系统的其他部分。然而,物理可访问性在至少两种情况下是一个关键因素:
攻击前设备分析 在许多情况下,设备可以被购买并分析其漏洞、加密秘密以及在攻击者控制的环境中的相关行为。物理访问使得攻击者可以提取设备的固件并进行详细的逆向工程,或通过印刷电路板 (PCB) 上的通信痕迹进行窃听,这可能揭示机密数据。
隐形操控 使用锤子“操控”机器通常会伴随噪音和可见损伤,人类可以察觉到。然而,任何能够改变设备或系统参数的物理接触或接近,如果没有对攻击系统进行深入分析,是无法察觉的,这可能代表一个非常强大的攻击向量,能够造成持久的影响。
对所有资产重复这个过程需要时间。然而,由于这种格式通常涉及来自多个领域的专家,因此在实践中通常会限制为一到几天。通过这一流程后,你会得到一组威胁场景,以供产品使用。表 1-4 展示了一个示例研讨会的部分结果。
表 1-4: 示例威胁场景
| ID | 资产 | 攻击者 | 威胁 | 影响 |
|---|---|---|---|---|
| TS01 | AS01 | AT04 | 当前没有完整的完整性和真实性保护;维护人员可以物理接触许多设备;知道如何打开设备并篡改固件 | 产品功能失常和客户投诉 |
| TS02 | AS02 | AT01, AT03 | 如果被购买,攻击者可以从设备固件中提取私钥;攻击者可以伪装成设备 | 向后台注入伪造数据 |
| TS03 | AS02 | AT01, AT03, AT04 | 攻击者可能在线发布私钥,损害公司声誉;世界上任何人都可以冒充我们的设备 | 媒体关注;甚至可能导致产品召回 |
| TS04 | AS03 | AT05 | 物理操控温度传感器;安全通信通道不能防范此类情况;温度始终显示为冷,而实际上持续超过限值 | 损坏案例中无法证明误用 |
| TS05 | . . . | . . . | . . . | . . . |
这些已知的场景将帮助你阐明所识别的问题,并与公司或管理层进行沟通。
无风险,无优先级
到目前为止,每个潜在的攻击向量和每个潜在的漏洞都已经收集完成,没有将任何内容作为“无关紧要”而丢弃。然而,清单可能会很长,而处理这些已识别问题的资源通常是有限的。现在是时候通过评估每个威胁场景的(至少)两个属性来筛选出重要内容:其发生的概率,以及如果发生,它的潜在影响。
在安全领域,几乎每一种可能发生的概率都可以用一个数字来描述。如果功能安全专家参与了你的跨学科研讨会,他们可能会提供这种方法论,并主张概率必须以数字形式表达。然而,没有人能够将想象中的人类对手提炼成一个有意义的百分比,因此,通过低、中、高等定性尺度对概率进行排名,可能是最合适的方式。
对于影响,我也倾向于使用简单的低、中、高三个等级。然而,在每个分析中定义这些术语的具体含义是非常重要的,这取决于产品、公司的规模以及你的商业案例。例如,你的团队可以商定将影响评级与损害数值关联,如表 1-5 所示。
表 1-5: 示例影响评级
| 影响 | 损害 |
|---|---|
| 低 | $10,000 |
| 中等 | $100,000 |
| 高 | $1,000,000 |
为了得到每个威胁场景的最终风险评分,我最后要解释一件事:低、中、高的概率和影响评分分别取值为 1、2 和 3。如果你将它们相乘,就能得到相应的风险评分。
在对所有威胁场景进行评级后,你将得到一个类似于表 1-6 所示的清单。为评分添加解释可以让大家更好地理解和推理,尤其是在几周后大家忘记了研讨会讨论的细节时。
表 1-6: 示例威胁场景评级
| 威胁场景 | 发生概率 | 影响 | 风险评分 |
|---|---|---|---|
| TS01 | 低(机会少) | 低(单一设备) | 1 |
| TS02 | 中等(离线攻击) | 中等(少数案例) | 4 |
| TS03 | 中等(需要专业知识) | 高(产品召回) | 6 |
| TS04 | 中等(财务利益) | 中等(如果出现多个案例) | 4 |
| TS05 | . . . | . . . | . . . |
在你和你的同事列出评分后的威胁场景清单后,你可以根据风险评分对其进行排序,或者以一个二维风险矩阵展示,其中概率和影响作为坐标轴。这种方法可以帮助设备架构师和产品经理更好地优先处理风险,因为总是没有足够的资源来实施每一项缓解措施并防范所有可能的威胁。
注意
电子表格和专门的威胁建模软件可以帮助你创建和可视化这里描述的威胁场景。但无论你使用什么方法,识别资产及其保护目标、创意思考攻击者及其目标,并判断收集的威胁场景的相关性,最终还是由你来决定。
安全需求与安全架构
威胁建模和风险分析对于你自己公司来说至关重要,它帮助区分设备所需的必要保护需求和仅仅是附加的保护需求。然而,重要的是要理解,你的组织并不是唯一的利益相关方。客户、认证机构和政府等可能会对你的产品施加额外的安全要求。
其中一些要求可能与你自身的兴趣重叠,而另一些则可能与你的兴趣相对立。确保处理你识别出的最高优先级风险,但也要在安全需求过程中考虑外部要求,如法律义务、客户需求和行业标准。
如果您的目标是产品认证,那么这是您应查看所选证书详细要求的时刻。例如,工业组件制造商可以将其设备的安全要求与 IEC 62443 Part 4-2 所要求的保护特性对齐。但即使没有明确的认证视角,像 ARM 的平台安全架构(PSA)1 级问卷这样的文档也可以作为指导和灵感,帮助为您的特定产品制定稳固的安全要求。
此外,安全要求总是与其他要求竞争,例如设备性能、向后兼容性以及没有安全特性的组件的更低价格。因此,您需要在早期阶段考虑与其他要求的任何冲突。
在识别安全要求的过程中,通常忽视了产品整个生命周期的考虑——从开发到生产,再到现场使用和退役。每个阶段都有其自身的要求和依赖关系。然而,如果设备的所有权可能会发生变化,或者设备上的机密数据在销毁之前从未讨论过如何处理,这时可能就没有为此类目的制定相应要求,这可能会导致后续的麻烦。
风险处理
在定义安全架构时,所有关于威胁、风险和要求的前期工作都必须汇集成一系列技术、保护措施和组织安排,最终确保产品的安全性。您可以通过多种方式来应对风险:
降低 处理风险的常见方法是减少它们。集成适当的保护措施可以减少成功攻击的概率,或限制可能造成的损害。这两种策略都可以降低某一威胁场景的风险。
消除 有时,移除软件组件、接口或产品特性可以完全消除某种风险。从安全角度来看,这一策略是完美的,但从商业和市场营销的角度来看,通常是一个艰难的决定。
转移 您可以将安全风险从公司转移到供应商,或从制造商转移到产品用户。通常,这种转移需要适当的文档和/或法律协议,但它可能进一步促进透明度,并增强合作伙伴和客户的意识。
接受 公司中的责任方可以接受风险,如果减轻风险的成本高于处理实际发生的安全事件的费用,那么这可能是一个选择。
安全开发原则
尽管本书的第二部分和第三部分旨在支持架构师在决定某些技术安全特性时的取舍,但在开发安全架构时,您还应该牢记概念性原则。
执行深度防御
每一项保护措施总有一天可能会失败,要么是因为遭遇了一个复杂的攻击者,要么是因为遇到了特殊情况。安全架构应实施多层保护。如果某一层破损,剩余的层次可以确保安全,或至少限制潜在的损害。
使用经过验证的安全技术或组件
一些公司拒绝使用来自外部方的现有技术和组件,即使它们质量很高。当然,在某些情况下,从零开始开发一个安全组件是合理的。然而,这样做可能涉及陡峭的学习曲线,并且可能会有许多失败。如果没有合理的理由,不妨使用已经证明稳健且安全的软件和硬件。
实施最小权限
对于稳固的安全架构,所有涉及的角色和权限都必须有文档记录。关于授予或拒绝人员和服务访问权限时,应只允许特定角色访问完成任务所必需的资源。这种做法也有助于限制内部攻击的威胁潜力。
保持简单
复杂性和不透明性是安全的天然敌人,因为它们使得分析风险、识别攻击路径和实施有效的对策变得更加困难。保持设备和安全架构的简洁性,可以更好地确保安全措施达到你定义的保护目标。
减少攻击面
通过减法来增加!这一谚语在谈到产品的攻击面时尤为准确。一个不存在的无线接口无法被攻击,一个从 PCB 中移除的调试端口就无法成为物理攻击者的入口点。移除你设备架构中所有不必要的功能、接口、硬件和软件组件,或者至少移除进入市场的最终产品设计中的这些内容。
实践中的案例:舍弃功能
我曾经是一个安全咨询团队的一员,受邀参加一个新工业产品早期阶段的架构讨论。讨论的主题涉及从安全通信到设备机密的安全存储,再到设备在从一个客户站点移动到另一个站点时的定位追踪。当时,后者的议题非常关键,我们花了相当多的时间来讨论它。
最终,关于安全和国际法律的考量导致将该追踪机制从产品功能列表中移除。伪造追踪数据及类似攻击的风险被彻底消除,产品管理团队也重新获得了安心。
安全实现与安全测试
前面的部分主要关注做对的事情,而这一部分则关心把事情做对。自然,硬件和软件开发人员希望实现一个功能完整、可工作的产品。然而,如果安全性没有引起足够重视(甚至根本没人关心),那么你最终得到的可能是一个功能完全但却存在多个弱点和漏洞的产品,这些漏洞可能会被犯罪分子、研究人员或客户在市场发布后不久发现。
左移
避免这种情况的一个策略是左移,即将实现错误的检测提前到开发过程中的尽可能早的阶段,而不是等到产品发布后进行检测。除了能加速反馈循环外,这还降低了错误检测和修正的成本。
这些考虑在实施阶段的最初就已经非常重要,因为你的团队正在做出可能具有重大安全影响的重要决策,比如中央硬件组件、操作系统(OS)、安全编码规则等等。让我们来看两个例子。
硬件组件选择
物理产品的硬件开发通常比软件开发开始得更早。在某个时刻,项目的截止日期会到来,这个时候称为硬件冻结,之后你就不能再更改设备的硬件组件了。如果在这个截止日期之后发现硬件安全问题,往往会导致匆忙的修复、昂贵的重新设计或者不安全的设备。
在选择设备的基础硬件组件时,一定要考虑安全性要求。如果组件成本不是关键因素,选择具有更多安全功能的组件,而不是那些功能较少的组件,这样可以在后期开发过程中为你节省大量的麻烦和费用。
编程语言选择
如果你问 10 个人某个特定任务最好的编程语言是什么,可能会得到 11 个答案。然而,特别是在嵌入式系统和物联网设备领域,我们已经看到,C 语言为无数内存安全问题奠定了基础,从而导致了安全漏洞。根据来自 Android、Chromium 和 Microsoft 的统计数据,它们的 70%到 90%的常见漏洞和暴露(CVE)都可以追溯到内存管理问题。试想一下,如果不再需要进行代码分析工具、代码审查以及内存管理问题的事件处理,你能节省多少金钱和精力。
不足为奇的是,Rust 在嵌入式、系统级别以及高性能软件开发社区中越来越受到关注,因为它是克服使用 C 或 C++带来的安全问题以及所有相关成本的一种解决方案。
实践中:安全规划
我曾与一家工业自动化行业公司的隐形冠军进行了对话。我和我的团队分析了该公司的一款产品,惊讶地发现其 PCB 上有一个安全元件(SE)。当我就此话题向首席技术官(CTO)询问时,他解释说,芯片的成本并不关键,但安全性最终会成为关键因素,因此他们将 SE 集成到设备中,尽管在产品发布时并未启用。它们能够在稍后的时间为秘密存储和安全通信激活该功能;为未来的安全挑战做好准备是关键。
持续测试与分析
当然,初始设计决策无法预见所有可能的安全问题。安全实现的第二个要素是构建、测试和审查过程的强大自动化,尤其是在软件开发过程中,开发人员必须在发现代码中的弱点和潜在问题后立即获得反馈。虽然安全性并非自动化和持续集成(CI)的主要目标,但它们有助于提高质量并减少人为错误,而人为错误通常是安全漏洞的根源。
从安全角度来看,自动化 SDL 时必须涵盖以下领域(其中一些是软件特定的,但大多数也可以应用于硬件开发过程)。
第三方组件透明度
在硬件设计中,生成物料清单(BOM)是日常工作,但对某些产品的软件物料清单(SBOM)的需求正在增长。两者都主要不是为了安全目的而设计,但对于追踪第三方依赖项中的安全漏洞,包括软件库、微控制器和操作系统,它们非常重要。
静态安全测试
在软件开发中使用静态应用安全测试(SAST)是问题检测的重要组成部分。这些工具可以帮助你识别 C 语言中的不安全函数:它们可能会检测到在发布前必须移除的硬编码秘密,并突出显示可能易受常见安全风险(如 OWASP 十大)影响的代码段。这些静态代码分析工具可以无缝集成到 CI 管道中,并为开发人员提供及时的反馈。
动态安全测试
静态分析无法检测某些漏洞,因为相关问题源于软件或设备的不安全行为,这意味着动态安全测试是必要的。你可以在一定程度上自动化此测试,例如通过将构建好的软件部署到 CI 管道中的测试设备上,该设备可以接受测试用例的挑战。然而,由于测试空间巨大,且动态应用安全测试(DAST)需要特定的运行时环境,这项工作通常比静态分析需要付出更多的努力。
实现审查
虽然我们通常将代码审查用于软件过程,但同样可以将审查应用于设备硬件设计,以发现实施中的弱点、禁用组件的使用或容易遭受物理攻击的设计模式。尽管这是一个人为的任务,但自动化可以帮助你安排审查,或者在关键硬件或软件部分发生变化时触发审查。
攻击者即服务
所有描述的技术都旨在验证和确认明确的安全要求和保护特性,或者避免可能破坏设备安全的实施缺陷。然而,尽管安全实现和安全测试的趋势是融合成一个安全、渐进且敏捷的开发过程,但通常有一种安全测试方法是手动执行的,并且不在 CI 环境中:渗透测试。
在渗透测试中,内部或外部的安全专家扮演攻击者的角色。通过模拟对当前设备的攻击,他们尝试触发最坏情况的场景,以展示攻击的可行性、所需的努力和可能的攻击路径。反过来,这种测试使得制造商可以重新设计相关的产品部分并发现进一步的相关问题。
由于渗透测试通常是一个时间有限的服务,因此高效利用可用的测试天数非常重要。如果你为你的设备订购渗透测试,确保明确指定三个重要方面:范围;最坏情况;以及你期望进行黑盒、灰盒还是白盒测试。
范围
如果你没有为渗透测试设置明确的范围,测试人员将只是寻找进入设备的最简单方法。然而,这可能包括打开设备,而这可能远离你所设想的攻击模型。
明确你的期望。是否包括物理攻击?应该攻击哪些接口,哪些接口不在范围内?是否可以操作设备的固件?
重点不是将范围缩小到最小以获得一个积极的测试结果。明确范围的目标是得出最能支持你保护工作结果的测试结论。
最坏情况
通常,设备上有一些关键资产,这些资产如果被攻破,将导致严重损害。理想情况下,你应该在过程中的风险分析阶段已经识别了这些资产,而你可能最关心的是对这些资产造成影响的攻击。如果你没有明确指定它们,渗透测试人员可能会花费数天时间尝试寻找一种巧妙的方法来操作一个仅对内部用途相关的图形用户界面(GUI)。
黑盒/灰盒/白盒测试
黑盒攻击假设攻击者对设备一无所知,这在你想了解攻击者在短时间内能收集到关于某个产品的信息时是有意义的。然而,如果将这种方法延续数周,并实际上支付渗透测试员为你逆向工程一些你已经知道的信息,那就没有意义了。
改进制造商效率的一种方式可能是分阶段的方法。经过几天的黑盒测试后,测试人员可以提出初步结果,并建议可能的下一步(逆向工程)操作。然而,在此之后,制造商可以向测试人员提供关于协议、硬件和/或软件的信息,帮助他们继续进行宝贵的渗透测试工作。你可以多次重复这一过程,最终实现一个节省时间、更加可能发现有价值安全见解的流程。
实践中:渗透测试目标
“你能为我们的产品做一次渗透测试吗?”这是几年前我与一位工业合作伙伴开始的有趣旅程的起点。我本可以说:“当然!给我你们的一两台设备,我会看看能为你们做些什么!”但我选择发起了一次关于安全测试、安全开发生命周期(SDL)以及后者应该从何时开始的讨论——应该从一开始就开始。
幸运的是,我遇到了心态开放、充满动力的人,他们愿意建立一个 SDL,并真正改善设备的安全性。我们最终进行了全面的威胁与风险分析,并根据结果设定了缓解开发的优先级。关键是,仅仅依赖渗透测试结果来推动产品的安全性并不是一种合理的策略。这一点需要强调。
最后,别忘了,对于像物联网设备这样的实体产品来说,最后的实现步骤并不是在开发团队的办公室里完成的,而是在生产工厂的生产线上。在选择安全的生产环境、保护设备在转移到生产现场时的资产,以及执行生产后测试以验证保护功能是否正常激活方面,都是确保设备安全实现的关键步骤。
漏洞监控与响应
无论你的公司有多大、你为组织安全开发过程投入了多少努力,或者你的工程团队有多聪明,始终有一种可能性,那就是漏洞可能隐藏在你的设备中。有些漏洞可能是开发过程中由人为错误引起的,有些可能源自第三方组件,还有一些可能因为攻击者工具和方法的进步而出现,这是你无法预见的。如果你意识到这一点,最好的做法就是做好准备,并计划将接下来的各个阶段以结构化的方式推进。
报告漏洞
一些客户、渗透测试人员和安全研究人员确实关心你的产品安全,并会通过负责任的、协调的披露过程向制造商报告发现的漏洞。然而,如果你的网站没有列出安全联系方式,比如一个简单的 security@company.com,这些人可能会遇到麻烦,因为他们找不到可以负责的人来报告他们的发现。然后,他们可能会使用 info@company.com 联系方式,但消息可能会被淹没在收件箱中,你将永远不会得知已发现的问题。
在其他情况下,发现漏洞的人可能会转向国家或国际安全组织,甚至媒体以引起关注,这可能不是你想要的结果。请在你的网站上建立一个简单、显眼的安全联系方式。其次,一些人会将安全问题报告到邮件列表或漏洞数据库,而不是直接联系制造商。监控与你行业相关的来源对于尽早发现和了解漏洞至关重要。
审查和评估漏洞报告
漏洞报告可以从简短的电子邮件到详尽的分析文档不等。收到报告后,首先要审查它,检查它是否真的是一个安全问题或是设备的预期行为,或者研究人员是否犯了什么错误。如果你的产品容易受到描述的攻击方式的影响,接下来的任务是明确原因。这个阶段的结果应该是对当前漏洞的内部理解和评级。
修复或解决问题
如果问题的根源在于设备软件,可以通过更新来修复,你可以开发一个补丁,带有两个目标。一方面,它应尽可能全面地消除漏洞。另一方面,它不应影响设备的其他功能或属性。
此外,可以考虑通过设备重配置来缓解漏洞,这种配置甚至可以由客户自己完成。在某些情况下,硬件或不可更新的软件可能是安全问题的根源。这些情况难以处理,因为它们可能需要对每个设备进行物理访问,甚至可能需要产品召回。
测试
无论解决方案是软件补丁、硬件返工,还是配置性变通措施,都必须在正式推向所有设备之前进行测试。在某些情况下,更新可能会影响设备性能;而在其他情况下,软件的变更可能会将问题转移到设备的另一个部分,进而产生新的漏洞。确保你的解决方案确实能够做到它应有的效果。
披露解决方案
总有一天,您的修复方案将发布给客户。根据行业标准、发现问题的严重性以及您处理漏洞响应的效率,从最初报告到发布修复可能已经过去了一周或一年以上。然而,即便在那个时候,世界上也没有了解详细情况。确保您提出的解决方案附带对问题的有益解释,并清楚地指示客户、管理员或操作员应该采取的行动。如果您的产品对公众有影响,请做好回答媒体询问的准备。
避免未来的问题
安全开发过程是一个持续改进的循环。每发现一个漏洞,都应该引发对开发流程可能改进的讨论,以避免未来出现类似的问题。
建立信任
对于每个厂商来说,与发现并报告漏洞的人建立信任关系是非常重要的。这些人实际上支持您的安全开发过程,而不需要在您的薪资名单上。关于漏洞处理过程状态的清晰和定期沟通至关重要。
根据发现问题的严重性和范围,您可以考虑通过奖励或安全悬赏来表达您的感激之情。但即使您认为报告的问题只是小事,提供一点感谢可能也是值得的,以承认报告者的努力并鼓励他们未来发现更多关键性漏洞。
实践中:漏洞报告
我曾经是一个团队的一员,负责识别一家跨国公司产品中的多个漏洞。通过快速的在线搜索,我们找到了该公司亚洲总部提供的漏洞报告表格,并确信该公司已建立了稳固的漏洞响应流程。我们填写了所有细节,附上了我们全面的 25 页分析报告,并希望一切顺利。
我们很快收到了这样一封邮件:“感谢您的报告,但我们认为我们没有什么可做的。”我们有些震惊,试图再次强调我们发现了关键性问题,但没有成功。接着,我们将这些问题报告给了一个德国的安全组织,该组织将报告转交给了厂商的欧洲联系人,并强调了这些问题的重要性。几个月后,一组专家得出结论,认为亚洲的漏洞处理工程师错误判断了我们的报告,并为此分配了多个 CVE 编号。
然而,如果我们当时没有那么宽容,这个问题可能会被公开,网络犯罪分子可能会在几周内开发出漏洞利用,而该公司的声誉可能会受到严重损害,仅仅因为漏洞处理过程中的某个人说了句:“我认为这个问题不太相关。”
如果你仍然认为,“不,没人会查找或发现我们设备中的漏洞,”那么考虑一下如果有人发现了漏洞,后果会怎样。如果你没有为漏洞处理流程做好准备,可能会出现两种情况。设备漏洞可能因为没有人关心或觉得自己没有责任而得不到足够的关注,这可能导致现场攻击、客户方面的实际损害、恶劣的媒体报道以及你产品或公司声誉的丧失。或者,如果漏洞报告让你团队中的每个人甚至更多人都像吓坏了的鸡一样乱跑,漏洞响应就会变成一个混乱的过程,耗尽情感和资源,最终无法为问题提供可靠的解决方案。
总结
本章总结了组织在追求安全产品时应该遵循的重要活动和过程。一开始,我解释了你可以选择多个内容相似的指南。虽然微软和 OWASP 的建议主要针对软件产品,但 IEC 62443 第四部分显然是针对可以认证的工业组件。
所有安全开发过程的基本要求是,人们要意识到风险,并接受他们在日常任务中所需的安全培训。创建安全产品的关键是对资产、相应的保护目标、可能的攻击者、威胁和涉及的风险进行扎实的分析。你可以通过一个结构化的方法来解决这一任务,从而实现透明度、明确的信任和风险决策,并形成清晰的文档。
基于这些初步工作,你可以进一步实现产品特定的安全需求和个性化的安全架构。培养开发人员和工程师的安全实施习惯,比如安全编码,并定期通过安全测试检查实施结果。漏洞监控以及高效有效的漏洞响应流程,完整了高质量产品的安全生命周期。
如果你想深入了解安全开发过程及相关方法论,可以看看 Loren Kohnfelder 的《设计安全软件》(No Starch Press, 2021)以及 Adam Shostack 的《威胁建模:设计安全》(Wiley, 2014)。
第二章:密码学

密码算法的保障和能力常常听起来像是魔法。普通人无法验证这些特性,大多数工程师也做不到。在某些情况下,即使是密码学家也无法证明一个方案的安全性,但他们假设或相信其背后的数学问题很难解决。尽管如此,密码学算法仍是每个开发者和架构师应当了解并掌握的必要工具。
密码学一词来源于希腊语单词kryptós和gráphein的组合,意思是秘密写作。然而,今天的密码学远不止是保护机密消息,它还用于保护文件的完整性,推导出可靠的指纹信息,用于对数 GB 的数据进行验证,以及对文件和代码进行数字签名。
本章提供了现代密码学算法及其实际特性的务实概述,同时尽量减少数学公式的使用。我们将从一些基本原理开始,然后简要了解典型的对称算法和哈希函数。最后,本章以探索对称密码学领域的引人入胜内容作为结尾。
凯克霍夫斯原理
奥古斯特·凯克霍夫斯(Auguste Kerckhoffs)是 19 世纪的一位荷兰密码学家。在安全领域,他因提出改善法国军队实际密码学的建议而闻名。1883 年,他发表的六项建议中,有一项被称为凯克霍夫斯原理:“系统不应依赖于保密性,且可以被敌人窃取而不会造成麻烦。”
对于密码算法而言,这一原理意味着加密、解密或签名等过程不应保密,任何人都不应依赖这些保密性来保证安全。系统中唯一的秘密应是一个保密的密码密钥。
如今,这一点似乎不值得特别提及,因为所有相关的密码算法都已在国家或国际层面上标准化,并且所有算法都已公开,供每个人阅读和分析。然而,当涉及到工程软件和设备时,一些开发者仍然违反这一原理。他们发明了自己的“加密”程序,并认为安全性是“因为没人知道它是怎么运作的”而实现的。他们有时这样做是出于性能考虑,但更多的时候则是因为缺乏扎实的密码学知识。然而,“可以被敌人窃取”这一说法,也可以理解为“可以被攻击者逆向工程”,这会破坏这种“解决方案”的安全性。
如果你发现自己在考虑实现一个自定义函数,可能包括一些神秘的值和异或操作来实现安全性,请立刻停止思考!这叫做安全通过模糊性,它只会让你陷入麻烦。
安全级别
加密算法有多个参数和属性来描述和区分它们,但其中一个是最为核心的:安全级别,它通过特定的位长来表示——例如,64 位、80 位或 256 位。这个实际的衡量标准使你可以比较算法及其加密强度。但如果一个特定算法有 128 位安全级别,这到底意味着什么呢?
该级别描述了攻击者为了突破算法保护目标所需付出的努力。通常,这种努力涉及测试大量数据集以找到正确的解决方案,比如一个秘密解密密钥。如果一个算法具有 128 位安全级别,那么攻击者的搜索空间就是 128 位大,这意味着攻击者必须进行最多 2¹²⁸次尝试才能在数据海洋中找到目标。
对于设计良好的对称加密算法,密钥长度可以直接转换为算法的安全级别。然而,如果密码学家发现算法存在缺陷,安全级别可能会发生变化。在这种情况下,算法的安全级别可能远低于密钥长度。
此外,请记住,攻击者不断提升其性能。现代的暴力破解攻击利用数千个云实例来高效地搜索密钥。近年来,对 64 位密钥的攻击已成功,进一步增加了对安全级别稳固选择的需求。
如果你想设计安全且持久的设备,请跟上当前密码学安全级别的推荐(见* www.keylength.com *)。根据写作时的经验法则,128 位安全级别被认为适合实际的安全工程,而 256 位通常用于高安全性应用。
警告
虽然对称加密的安全级别通常等于其密钥长度,但并非总是如此。此外,非对称加密的安全级别完全不同;例如,具有 2048 位的密钥可能只提供 112 位的安全性,正如你将在本章后面看到的。
对称加密算法
对称加密的起源可以追溯到著名的罗马人凯撒,据说他曾使用简单的字母替换加密法使信息变得无法理解。从那时起,对称加密的基本原理就没有改变。它遵循这样的理念:明文消息可以通过使用加密算法Encrypt()和加密密钥将其加密成密文,正如图 2-1 所示。

图 2-1:对称加密的基本原理
解密操作Decrypt()使用相同的秘密密钥来反转加密过程并恢复原始消息,因此这种加密方式被称为对称加密。
数据加密标准
快进到 2000 年。在许多情况下,对称加密现在由分组密码处理,这些密码将一块明文加密为一块密文。第一个公开标准化的分组密码是数据加密标准(DES),也称为数据加密算法(DEA)。它基于所谓的Feistel 网络,并且只有 56 位的密钥。今天,通过专用硬件,这个密钥空间可以在几小时内完全搜索,这使得它对于现代应用来说绝对不安全。
三重 DES(3DES)是 DES 的扩展,使用三个 56 位的密钥,并对明文或密文应用三次 DES。考虑到总密钥长度为 168 位,块大小为 64 位,并且已知的加密弱点,3DES 被认为只能提供 112 位的安全级别,并且不应再用于新的设计中。
警告
一些现代的加密库仍然提供 DES 和 3DES。然而,如果你没有非常充分的理由——例如,必须的向后兼容性——不要使用任何基于 DES 的算法。
高级加密标准
当前对称加密的首选是高级加密标准(AES),最初名为Rijndael。这一 DES 的继任者经过 1997 年到 2000 年由 NIST 组织的密码学竞赛过程后,作为《联邦信息处理标准》(FIPS)197 号和 ISO/IEC 18033-3 标准化。AES 基于置换-替代网络(SPN),具有 128 位的块长度,并可以使用三种密钥长度:128 位、192 位和 256 位。
如图 2-2 所示,AES 在一个 4×4 字节矩阵上操作,这个矩阵被称为AES 状态。

图 2-2:AES 状态的矩阵可视化
根据所选的密钥大小,这个状态会经过一定轮数的处理:对于 128 位密钥为 10 轮,192 位密钥为 12 轮,256 位密钥为 14 轮。以下是基于主要 AES 功能的基本加密过程:
密钥扩展 原始密钥被扩展为多个 128 位的子密钥,每轮一个,外加一个初始密钥。
初始轮 作为准备步骤,函数AddRoundKey()被应用于输入明文,以获得新的状态。
主轮 根据密钥长度,执行 9、11 或 13 轮主操作,以下操作依次发生:SubBytes()、ShiftRows()、MixColumns()和AddRoundKey()。
最后一轮 在最后一轮中,仅调用SubBytes()、ShiftRows()和AddRoundKey()。MixColumns()被省略。
实现这种强加密的四个操作非常简单。SubBytes()函数将 AES 状态中的每个字节替换为由查找表(称为S-box)生成的相应字节,如图 2-3 所示。

图 2-3: SubBytes() 转换
图 2-4 显示了 ShiftRows() 变换将 AES 状态矩阵的第二行向左移 1 字节,第三行向左移 2 字节,第四行向左移 3 字节。第一行保持不变。

图 2-4: ShiftRows() 变换
MixColumns() 操作对 AES 状态矩阵的每一列应用线性变换,如图 2-5 所示。这样得到 4 个字节,分别表示每一列的新状态。

图 2-5: MixColumns() 变换
每一轮中,AddRoundKey() 操作将 AES 状态的每个字节与给定轮密钥的相应字节进行异或操作,如图 2-6 所示。

图 2-6: AddRoundKey() 变换
对于解密,子密钥的顺序被反转,并且调用 SubBytes()、ShiftRows()、MixColumns() 和 AddRoundKey() 的逆函数。
经过 20 多年的广泛研究,至今没有人发现 AES 架构存在具有实际意义的攻击。它已被所有主要的加密软件库所支持,甚至像 8 位微控制器这样的小型系统也可以在合理的性能下使用它。AES 被用于笔记本电脑的磁盘加密,也用于安全互联网通信中的有效载荷加密。每当涉及对称加密时,除非有充分的理由,否则你应该选择 AES。
工作模式
由于 AES 是一个分组密码,它自然只加密或解密单个数据块,但许多应用程序的输入数据要比一个 128 位的块多得多。因此,AES 必须在某种工作模式下使用,该模式定义了加密和解密多个数据块的过程。这里介绍的模式在 NIST 的特殊出版物 800-38A中有所定义。
电子密码本模式(ECB)
有些人可能会想:“为什么不按块加密数据呢?”这种简单的做法正是电子密码本(ECB)模式所做的。它将消息的前 128 位作为第一个明文块,将其加密为第一个 128 位的密文,然后继续对所有可用的 128 位数据块按此方式处理,如图 2-7 所示。

图 2-7:ECB 模式下的加密
然而,这种方法的问题在于相同的输入数据块会加密为相同的密文块。因此,块与块之间的关系(也可能包含敏感信息)得以保持。图 2-8 展示了这一现象。

图 2-8:比较 ECB 模式与计数器(CTR)模式
当图像使用 ECB 模式加密时,具有相同值的明文像素仍然会映射到具有相同值的密文像素。图像信息仍然是可以理解的。而对于其他操作模式,如计数器模式,情况则不同,如第 34 页的“计数器模式”一节所述。
密码分组链接模式
密码分组链接(CBC)模式通过将第一个块的密文与第二个块的明文进行异或,依此类推,从而打破了明文与密文之间的关系。图 2-9 展示了这一基本原理。

图 2-9:CBC 模式中的加密
从安全角度来看,CBC 模式显著优于 ECB 模式,但它有一个新的缺点:后续加密之间的依赖性使得并行实现的效率降低,从而限制了性能。
计数器模式
一种有趣的模式,既不允许密文之间的关系,但又便于高性能的多核实现,叫做计数器(CTR)模式。如图 2-10 所示,明文块本身并没有加密,而是将一个nonce(一次性数字)与从 0 开始的计数器值连接起来。

图 2-10:CTR 模式中的加密
这种加密的结果会与明文进行异或操作,从而得到密文。对于每一个后续的明文块,计数器值会增加 1,以支持密文的变化,如图 2-8c 所示。这种方法甚至提供了另一个优势:加密和解密都使用所用分组密码的Encrypt()函数,无需实现Decrypt()。
与 ECB 模式不同,CBC 和 CTR 模式需要额外的输入参数:初始化向量(IV)或 nonce。两者的目的都是使每次加密都具有唯一性,因此它们不应当重复使用。此外,只有接收方也能访问对应的 IV 或 nonce,解密才是可能的。但由于它不携带机密数据,因此可以与密文一起以明文形式传输。
警告
实际上,仅将 IV 或 nonce 设置为 0 或一个固定的随机数是很有诱惑的。然而,这么做会显著削弱这个加密原语的强度。花费额外的精力来实现一个适合的生成器,以确保生成唯一的值。
与操作模式 CBC 和 CTR 一起,AES 非常流行。然而,如果你有特殊需求,其他更有趣的对称加密算法可能会有所帮助。例如,由密码学家 Daniel J. Bernstein 开发的现代流密码 Salsa20 和 ChaCha20,具有简单的设计,并在纯软件实现中提供高性能。如果你的硬件不支持 AES,但你需要获得尽可能高的性能,这些算法可能值得一试。
哈希函数
哈希函数在密码学算法中有些另类。单独使用时,它们并不追求经典的保护目标。它们的目标是将更多或更少任意大的输入数据映射为一个固定长度的二进制序列,称为哈希值或摘要。
然而,设计这样函数并不简单。它们必须满足一系列强要求:
原像抗性 术语原像指的是映射到给定哈希值的哈希函数的正确输入数据。这个要求表示攻击者不应该能够找到适合现有哈希值的输入数据。这也是为什么哈希函数也被称为单向函数的原因。没有人应该能够反转它们。
第二原像抗性 此外,具有消息及其哈希值的恶意行为者不应该能够找到第二原像——即另一个映射到相同哈希值的消息。
碰撞抗性 自然,哈希值碰撞是不可避免的,因为哈希函数的输入空间大于其输出空间。哈希函数的最强假设是找到任何两个映射到相同哈希值的消息应该是实际不可能的。
从设计上讲,哈希函数不使用秘密密钥,这意味着每个人都可以使用它们并将其应用于手头的所有数据。因此,我们必须以不同的方式来确定哈希函数的安全级别。安全级别描述为找到碰撞的难度,这本质上是一个搜索空间问题,类似于为加密算法找到正确密钥的问题。
基于所谓的生日悖论和rho 方法,密码学家们已经证明,对于设计良好的哈希函数,我们可以估算其安全级别为输出大小的一半。例如,对于一个输出为 160 位的哈希函数,安全级别的估算值约为 80 位。
在这个时候,你可能会想,为什么我们需要哈希函数及其加密映射过程来确保设备安全?原因很简单:它们是许多安全应用的一部分,如数字签名生成和验证、密钥派生算法、安全密码存储等。
哈希函数的第一个实际实现出现在 1990 年代。MD4、MD5 和 SHA-1 是三个突出的代表。然而,与此同时,研究人员已经发现了找到这三种(以及其他哈希函数)碰撞的方法,因此你不应该再在现代设计中使用这些遗留算法。
目前,最广泛使用的哈希函数家族是 SHA-2,它是 SHA-1 的继任者。它在FIPS 180-4标准中进行了描述,并有四个成员:SHA-224、SHA-256、SHA-384 和 SHA-512。数字表示这些函数的输出长度,因此 SHA-224 与 SHA-256 非常相似,除了初始值不同以及最终哈希值被截断为 224 位。SHA-384 与 SHA-512 之间也有类似的区别。
SHA-256 处理 512 位(16 个 32 位字)的数据,进行 64 轮操作。它在抵抗碰撞方面具有 128 位的安全性,因此是一种安全且高效的选择。SHA-512 则用于高安全性领域,如联邦和军事用途。它处理 1024 位(16 个 64 位字)的数据,进行 80 轮操作,且相较于 SHA-256,性能较低。
由于 SHA-2 系列基于类似于 MD5 和 SHA-1 的架构,密码学家对它的长期安全性持怀疑态度。2007 年,NIST 宣布了一项密码学竞赛,旨在找到一个候选算法,以标准化为 SHA-3。一个重要的要求是算法的设计应基于与 SHA-2 不同的原语。五年后,Keccak算法被选为获胜的哈希函数。随后,它被发布为新的 SHA-3 标准,收录于FIPS 202中。
SHA-3 也有四个版本:SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。同样,它们的输出值长度(以位为单位)由名称中的连字符后面的数字表示。它们的性能和安全性与 SHA-2 系列相当,但它们基于完全不同的算法基础,正如 NIST 所期望的那样。
注意
即使 SHA-3 是更新的标准,你也不需要立即从 SHA-2 迁移到 SHA-3。它们是可比的,因此你可以根据所需的库和应用需求自由决定使用哪一个。
消息认证码
关于加密的一个常见误解是它能防止篡改。事实并非如此。即使你使用 AES 和最先进的操作模式,并且攻击者无法从结果密文中读取任何内容,攻击者仍然能够篡改单个位或整个消息,而不会被块加密算法检测到。加密只保护机密性,而不保护完整性。
消息认证码(MAC),也称为消息完整性码(MIC),是对称密码学工具中的另一个原语,它旨在保护消息的完整性和真实性。它通过消息和一个秘密密钥创建认证标签。随后,任何拥有秘密密钥的人都可以验证消息及其对应的认证标签的正确性。
一种常见的生成 MAC 的方式是基于哈希的消息认证码(HMAC)。HMAC 结构,也称为带密钥的哈希函数,在 RFC 2104 中定义。以下公式展示了它的组成:
HMAC = 哈希((密钥 ⊕ 内填充) ∥ 哈希((密钥 ⊕ 外填充) ∥ 消息))
内填充(ipad)是一个字节串,0x3636...36,包含与所使用哈希函数的输入块大小相同数量的字节。它与至少具有与底层哈希函数安全级别相同的密钥进行异或。结果与要保护的消息连接在一起,然后进行哈希运算。此操作的摘要随后附加到与外填充(opad)(一个字节串0x5c5c...5c,其长度与 ipad 相同)进行异或的结果上。对结果字节进行哈希运算得到最终的 HMAC 值。
举个例子,如果选择 SHA-256 作为 HMAC 的哈希函数,那么该加密算法就叫做 HMAC-SHA-256。内外填充的长度是 512 位,即 64 字节。密钥应至少为 128 位长,HMAC 的长度为 256 位,即 32 字节。
注意
实际上,你可能会遇到基于 MD5 或 SHA-1 的 HMAC 结构。即使这些哈希函数已经证明存在碰撞攻击,它们仍然适用于实际的 HMAC 实现,因为攻击者需要获得大量来自同一个密钥的认证标签才能破解特定实例。
基于密码的消息认证码(CMAC)使用 CBC 模式下的分组密码来计算 MAC。一个基于 AES 的变体,称为AES-CMAC,在 RFC 4493 中进行了规定。在那里,保护的消息使用 AES 在 CBC 模式下进行处理,除了最后一个消息块,出于安全原因,该块被专门处理。最终的密文作为认证标签,而所有其他密文则被丢弃。
这种解决方案不像 HMAC 结构那样流行,但从性能角度来看可能会很有趣,特别是在那些内置 AES 加速器的设备上。
认证加密
对于安全通信,保密性、完整性和真实性是常见的保护目标。在前面的部分中,我们考虑了如何使用分组密码和 MAC 来实现这些目标,但仍然有两个实际问题需要解决:“我们如何将它们结合起来?”以及“将密码和 MAC 组合使用是否比单独使用它们更高效?”
策略和要求
在结合加密和 MAC 生成时,我们可以使用三种策略。所有这三种策略都假设使用安全的密码和 MAC 算法,并且每种算法都使用自己的密钥,但在执行操作的顺序上有所不同:
加密并 MAC 这种方法从相同的明文并行生成密文和认证标签,这可能带来性能优势。然而,将 MAC 算法应用于明文可能会通过认证标签泄露明文的信息,因为 MAC 算法并不设计用于保护输入数据的机密性。然而,对于现代的 MAC,如 HMAC-SHA-256,这个问题不存在。另一个问题是密文的完整性没有得到保护。篡改过的密文必须先被解密才能检测到篡改,这可能允许攻击者利用解密过程中的实现漏洞。
先 MAC 再加密 在这种变体中,认证标签是从明文生成的,并附加到明文上。结果随后被加密以获得最终的密文。在这种情况下,MAC 算法不会泄露任何信息,但密文仍然可以被篡改,在解密之前无法检测到。
先加密再 MAC 这是一种强有力的消息完整性和保密性保护策略。首先,对明文进行加密,然后从加密后的密文中生成 MAC。采用这种方法,认证标签不能泄露任何关于明文的信息,同时,我们可以在解密给定数据之前验证密文的正确性。
加密实践者总是在寻找性能提升的机会,因为安全措施应该尽可能减少性能开销。因此,认证加密(AE)领域从结合密码算法和 MAC 算法以提高处理速度中应运而生,这并不令人惊讶。
与“普通”密码算法相比,AE 算法在加密过程中不仅计算密文,还计算一个认证标签。在解密时,必须先验证密文和认证标签的完整性,然后才能继续。
AE 的一个有趣扩展叫做带关联数据的认证加密(AEAD),它允许将额外的明文数据集成到生成认证标签的过程中。例如,序列号或不需要保密性的元数据。通过将这些关联数据与产生的密文绑定,确保了它们的完整性。
伽罗瓦计数模式
今天,最流行的 AEAD 算法是采用特殊伽罗瓦计数模式(GCM)操作的 AES 密码算法,它遵循加密-再 MAC 原则,并且在 NIST 的特殊出版物 800-38D中进行了规范。图 2-11 展示了它的基本功能。

图 2-11:GCM 中的认证加密
机密性保护与 CTR 模式非常相似,不同之处在于 GCM 不会使用第一次加密的结果来处理明文,而是在认证标签生成的最后一步进行处理。MAC 生成基于哈希函数GHASH,它基本上由一系列在固定参数哈希子密钥下进行的二进制 Galois 域乘法组成。该子密钥通过使用加密所用的 AES 密钥对全零明文块进行加密得出。
GCM 的随机数或初始化向量应为唯一的 96 位二进制字符串。GCM 对随机数重用非常敏感,这意味着如果攻击者能访问两个使用相同密钥和随机数生成的认证标签,他们就能伪造认证标签。因此,为 GCM 实现一个健壮的随机数生成机制至关重要。
除了 AES-GCM 外,你还可能会在该领域遇到许多其他的 AE 算法。例如,NIST 的特别出版物 800-38C中详细描述了基于 CBC-MAC 的计数器模式(CCM),以及 Phil Rogaway 设计的偏移码本(OCB)模式。由 Bernstein 设计的流密码 ChaCha20 与 MAC 算法 Poly1305 的结合也作为一种常见的 AEAD 解决方案,特别适用于仅软件实现,并且已经在 RFC 8439 中指定。
此外,从 2013 年到 2019 年举办的 CAESAR 竞赛(* competitions.cr.yp.to/caesar-submissions.xhtml *)产生了一套创新的 AEAD 设计——例如,面向资源受限设备或高性能应用的轻量级实现。
非对称加密
与使用单一的秘密密钥不同,非对称加密的基本思想是拥有一对密钥,并且每个密钥仅用于与另一个密钥互补的特定操作。例如,每个人使用一个密钥,称为公钥,来加密数据(图 2-12)。第二个密钥,称为私钥,属于单一实体,只有该实体才能通过使用其私钥解密之前生成的密文。非对称加密系统也被称为公钥加密。

图 2-12:使用非对称加密进行加密
然而,非对称加密不仅仅用于保护机密性。私钥操作还可以生成数字签名,它是一种用于给定数据的校验和,只有特定实体的唯一私钥才能计算。拥有相应公钥的每个人都可以验证这个签名,这不仅保护了受保护数据的真实性,还保护了其完整性,因为数据或签名的篡改会导致验证失败。图 2-13 说明了基本原理。

图 2-13:数字签名的生成与验证
这些根本不同的机会为我们已经习以为常的许多安全功能铺平了道路,比如安全的互联网通信。为了理解非对称加密中两种最流行代表的基本原理,避免不了一些数学知识,正如我将在下一部分中展示的那样。
RSA 加密系统
第一个且仍然最常见的非对称加密方案在 1977 年发布,并以其发明者命名:Rivest-Shamir-Adleman (RSA)。为了实现公钥和私钥操作之间的典型非对称性,RSA 实现了一种 陷门函数。通过这种方法,你可以轻松地将数据 A 转换为数据 B,但除非知道陷门,否则几乎不可能从数据 B 中推导出数据 A。
基本 RSA 数学
RSA 基于模运算,这意味着如果整数运算达到一个叫做 模数 的限制时,就会环绕回去。以下公式描述了 RSA 加密背后的所有“魔法”:
y = x^e mod n
x 明文和 e 公钥指数的 模幂运算 计算出 y 密文。n 表示模数。
解密过程与加密非常相似:
x = y^d mod n
模幂运算使用 d(私钥指数)对 y 密文进行处理,得到 x 明文。然而,朴素的指数运算方法,即对 y 执行 d – 1 次乘法运算,对于拥有数千位的 d 来说是完全不可能的。你可以通过一种叫做 平方乘法 的算法克服这个障碍。该算法基本上是逐位操作指数的二进制表示。在每一步,算法都会执行平方操作,但如果某个位为 1,它还会执行额外的乘法,使得 RSA 成为一种实际可用的加密机制。
数字 n 和 e 通常被认为是公钥的一部分,而 d 对应于 RSA 中的私钥。它们所描述的行为只有在 d 和 e 之间存在特殊关系时才可能,这种关系是在 RSA 密钥生成过程中建立的。在第一步中,需要随机选择两个大素数 p 和 q,它们的乘积得出模数 n = pq。利用 n,可以推导出欧拉函数 Φ(n) 的结果:
Φ(n) = (p – 1)(q – 1) = pq – p – q + 1 = (n + 1) – (p + q)
这个值对于 RSA 加密系统及其安全性至关重要,因为 e 和 d 之间的逆关系是在模 Φ(n) 的群中实现的:
ed mod Φ(n) = 1
在实际应用中,e = 65537 = (10000000000000001)[2] 是一个常见的选择,因为它简短且具有较少的 1 位数,这两者都能提高使用平方乘法算法时的性能。在选择 e 后,密钥生成的最后一步是计算 d,即 e 在模 Φ(n) 下的逆元。
了解这些细节后,可能会更清楚为什么 RSA 常常与整数因式分解问题一起提及。这个数学问题表明,寻找大数的因子是非常困难的。模数n就是这样一个巨大乘积。RSA 是安全的,因为从给定的n中找出p和q是困难的。然而,如果有人发现了解决这个问题的方法,攻击者就可以从分解后的 p 和 q 中计算出Φ(n),并计算出 e 关于 Φ(n) 的逆元,这样 RSA 就会被破解。
与对称方案相比,RSA 密码系统的数学基础使得估算其安全级别更为困难。在 2020 年的 《特别出版物 800-57》 中,NIST 的加密专家评估了 RSA 的安全级别,如 表 2-1 所示。
表 2-1: RSA 安全性估算
| 安全级别 | 密钥长度 |
|---|---|
| ≤ 80 位 | 1,024 位 |
| 112 位 | 2,048 位 |
| 128 位 | 3,072 位 |
| 192 位 | 7,680 位 |
| 256 位 | 15,360 位 |
显而易见,有两个重要的特性。所需的密钥长度远大于所达到的安全级别。它们之间的关系不是线性的,但与投入的密钥位数相比,安全级别的提升显著更慢。在撰写本文时,2,048 位是常见设置,但对于长期使用,建议使用 4,096 位 RSA 密钥。
实际应用中的 RSA
然而,RSA 的基本结构并不完全适合实际加密,因为如果攻击者持有两个密文,他们可以创建一个新的密文,该密文是两个原始明文相乘后的正确加密。最优非对称加密填充 (OAEP) 方案就是为避免这个弱点而开发的。在与 RSA 结合时,它被称为 RSA-OAEP 或 RSAES-OAEP,并在 NIST 的 《特别出版物 800-56B》 中有所规定。
简而言之,OAEP 使用一个随机字符串和两个哈希函数来处理明文,通常是对称密钥,并生成一个填充后的明文版本,该版本随后用于 RSA 加密。对于解密,密文按照标准 RSA 进行解密,但随后必须通过使用前面提到的两个哈希函数恢复初始消息和随机值。最后,必须验证计算结果的正确结构,才能使用明文。
使用基本 RSA 方程的数字签名也存在类似的问题。基于特定消息的有效签名,攻击者可以为他们选择的消息创建有效签名。概率签名方案 (PSS),即 RFC 3447 中定义的 RSASSA-PSS(附录式 RSA 签名方案),可以防止这种攻击。
PSS 在签名方案的输入数据中注入额外的随机填充,即要签名的消息的哈希值。再次强调,生成消息哈希的填充版本需要两个哈希函数和预定义的编码。在验证过程中,必须恢复中间值并检查填充是否正确,以验证签名的有效性。
Diffie-Hellman 密钥交换
当只有对称密码学可用时,人们必须通过安全通道交换密钥——面对面、密封信件或已保护的通信线路。这一过程非常不便,且存在多个实际缺陷。
幸运的是,1976 年,Whitfield Diffie 和 Martin Hellman 发表了他们的想法,介绍了如何在不安全通道上建立两个只能通过不安全通道进行通信的当事方之间的共享秘密,现在通常被称为Diffie-Hellman (DH)密钥交换。DH 不是在特定位置生成密钥,然后应用密钥传输机制(例如 RSA 加密),而是通过两个实体之间的密钥协商协议创建共享密钥。
数学之美
DH 背后的数学基础是基于一个群
,在此群中所有操作都是模素数p进行的,原始元素被标记为g。图 2-14 说明了建立共享秘密的过程。

图 2-14: Diffie-Hellman 密钥交换的步骤
首先,双方通常称为 Alice 和 Bob,分别选择秘密数a和b ➊。然后,他们派生相应的公共值A和B ➋并交换它们 ➌。随后,他们将对方的公共值提升到他们自己的私密值的幂次 ➍ 以获得共享秘密k[AB]。
DH 的安全性依赖于离散对数问题 (DLP):从A = g^a mod p中获得a是困难的。从公开传输的值A中推导出私密数a显然会破坏该协议的安全性。然而,由于这几乎是不可能的,窃听 DH 协议的攻击者不会获得任何有用的信息。与 RSA 相比,关于 DH 密钥大小的实现安全级别实际上是相同的。
中间人攻击
这个基本版本的 DH 也被称为 匿名 Diffie-Hellman,因为 Alice 和 Bob 无法验证彼此的身份。这个事实可以被对手利用,通过 中间人(MITM) 攻击:一个恶意行为者拦截了原始通信双方的交流,丢弃了它们交换的公钥值 A 和 B,并分别与 Alice 和 Bob 进行一次 DH 协议运行。最终,Alice 和 Bob 认为他们的密钥协商成功,并且可以安全地通信。然而,他们现在与攻击者共享一个密钥,攻击者能够读取和篡改它们之间流动的所有信息。
解决这个问题的方法是引入 认证 Diffie-Hellman,它基于这样的假设:Alice 和 Bob 都拥有一对长期的公钥对,可以通过交换 A 和 B 的数字签名值来互相证明身份。在这种情况下,中间人(MITM)攻击者将无法悄悄地介入而不被发现。在这种方法的变体中,其他协议数据也会被签名和验证,但在计算并使用共享密钥之前,始终需要验证对方的身份。
DH 的第二个特性是你不应将值 a 和 b 视为长期密钥。将它们用于多个密钥协商会违反 完美前向保密性(PFS) 的常见要求。这意味着,即使长期密钥在未来被破解,通过密钥交换建立的会话密钥仍应保持安全。你应该将私有数值 a 和 b 视为 临时密钥,并且只使用一次。DH 的相应版本通常被称为 Diffie-Hellman 临时密钥(DHE)。
你可以在 NIST 的特别出版物 800-56A中找到关于 Diffie-Hellman 变种和其他密钥协商方案的详细信息。
椭圆曲线密码学
RSA 和 DH 提供了许多有意义的非对称加密机制,但它们有两个显著的缺点:密钥较长和性能要求较高。1985 年创立的 椭圆曲线密码学(ECC) 领域承诺能够显著减少非对称加密的缺点。
然而,约 20 年后,ECC 才广泛应用于实践中。与 RSA 和 DH 相比,ECC 背后的数学相当复杂,且 ECC 的采用受到了 Certicom 公司的阻碍,因为该公司拥有一系列专利,如果使用 ECC,则需要许可。幸运的是,现在大多数这些专利已经过期。
曲线背后的数学
简而言之,椭圆曲线被认为是一组具有 x 和 y 坐标的点——例如,表示为 P(x, y)。x 和 y 的值是一个模素数 p 的群中的整数,通常表示为 GF(p),即 伽罗瓦域。椭圆曲线的方程描述了所有属于它的点。
在密码学中,只有少数曲线方程具有实际意义。以下是用于NIST 标准的 Weierstrass 曲线(如 P-256),其参数为a和b:
x² = x³ + ax + b
Montgomery 曲线使用参数A和B,它们特别著名,因为有一个重要的成员 Curve25519。以下方程描述了它们:
By² = x³ + Ax² + x
Edwards 曲线,其参数为d,是另一种有趣的曲线类型,Ed448-Goldilocks 就基于它。以下是这些曲线的一般方程:
x² + y² = 1 + dx²y²
你可以通过三种方式处理椭圆曲线上的点:
点加法 将两个点P和Q相加,得到一个新的点R,该点位于同一曲线上:R = P + Q。
点加倍 将给定的点P乘以 2,得到一个新的点R:R = 2P。
标量乘法 取一个点P,并将其与一个整数(标量)k相乘,得到一个新的曲线点R:R = kP。
ECC 中的标量乘法类似于 RSA 的模幂运算。此外,标量乘法的朴素方法,即将一个点P加上* k* - 1 次,对于* k是一个有几百位的大数时是不可行的。因此,必须使用类似“平方并乘”算法的“加倍并加”算法来高效计算。简而言之,k*是按位处理的,对于每一位,都会执行一次点加倍操作。如果某一位为 1,则执行额外的点加法操作。总之,这使得 ECC 密码原语可以实际应用。
椭圆曲线的设计方式使得椭圆曲线离散对数问题(ECDLP)非常困难。这个名字表明,它与作为 DH 基础的离散对数问题(DLP)类似,确实如此。然而,在 ECC 的世界中,你可以将问题描述为给定点P和R = kP时,求解标量k。来自 NIST 的特殊出版物 800-57(2022 年)的表 2-2 显示了 ECDLP 与 RSA 和 DH 的基础问题之间的显著差异。
表 2-2: ECC 的安全性估算
| 安全级别 | 密钥大小 | RSA/DH 的密钥大小(供比较) |
|---|---|---|
| ≤ 80-bit | 160-bit | 1,024-bit |
| 112-bit | 224-bit | 2,048-bit |
| 128-bit | 256-bit | 3,072-bit |
| 192-bit | 384-bit | 7,680-bit |
| 256-bit | 512-bit | 15,360-bit |
ECC 通过使用更小的密钥尺寸实现相同的安全级别。此外,随着密钥长度的增加,相应的安全级别线性上升,因此将曲线大小从 256 位增加到 512 位也意味着安全级别翻倍。
选择的痛苦
与许多其他加密算法不同,ECC 要求实现者不仅要选择密钥大小,还要选择要使用的特定椭圆曲线。“安全”曲线必须满足某些数学要求,因此从经过严格分析的常见标准化选项中选择一条曲线是合理的。
另一方面,你在选择过程中可以考虑的一个标准是曲线参数的来源。虽然美国国家安全局(NSA)创建了素数曲线 P-256,并且 NIST 在 2000 年将其标准化,它在实践中仍然是最受欢迎的曲线之一。其参数b是一个 256 位数字,只有 NSA 知道它是如何以及为什么被选择的。其他 NIST 曲线也存在同样的问题:P-192、P-224、P-384 和 P-521。这可能不是一个关键问题,但对于在关键基础设施中运行的设备来说,可能是一个相关问题。
Curve25519 目前是最受欢迎和最可信赖的椭圆曲线。它由伯恩斯坦(Bernstein)在 2006 年提出,因其具有两个显著特点而广受关注。首先,它在软件实现中对高性能有着强烈的关注。其次,它不依赖于随机选择或晦涩的曲线参数。未来,像 Ed448-Goldilocks 和 Curve41417 这样的 Edwards 曲线也可能受到关注,因为它们提供了超过 200 位的安全级别。
ECC 的实际应用
ECC 的最常见应用之一是基于椭圆曲线数字签名算法(ECDSA)的数字签名,该算法在FIPS 186-5中定义。此用例的基本设置是两方已就特定曲线及其参数达成一致。签名方持有私钥d,这是一个秘密整数,验证方则可以访问相应的公钥P = dG,其中G是所选曲线的基点。
对于签名生成,签名者将消息进行哈希处理,选择一个随机的临时密钥,并将二者结合处理,以获得一个包含两个值的唯一签名,总大小为曲线阶的两倍——例如,对于一个 256 位曲线,签名大小为 512 位或 64 字节。(验证过程的细节相当复杂,超出了本书的讨论范围。)
一般而言,ECDSA 签名在性能和安全性方面比 RSA 签名更为优选,只有一个例外情况:如果签名验证具有最高优先级,而签名生成的频率非常低——例如,仅在开发过程中对固件镜像进行一次签名——并且验证是时间敏感的,因为它发生在设备的启动过程中。
除了数字签名,ECC 是现代密钥交换和密钥协商方案的核心部分,正如 NIST 的 Special Publication 800-56A 所描述的那样。它们也被称为 椭圆曲线迪菲-赫尔曼(ECDH) 和 椭圆曲线迪菲-赫尔曼瞬态(ECDHE)。在这些方案中,DH 的模幂运算被基于椭圆曲线的标量乘法所替代,其余部分与 DH 非常相似。这种替代方法相比传统的 DH 提供了显著的性能提升,特别是当你需要定期进行大量密钥协商握手时,例如服务器处理大量连接时。
最后但同样重要的是,你可以使用 ECC 进行公钥加密,但在实际中很少使用。椭圆曲线集成加密方案(ECIES) 就是为此目的设计的。简而言之,它使用接收者的公钥生成一个共享的 ECDH 密钥,并从中派生出一个对称密钥。然后,使用该密钥通过 AE 加密算法加密消息,产生密文和认证标签。
总结
本章开始时介绍了加密学基础,如 Kerckhoffs 原则,认为加密系统中唯一的秘密应是加密密钥,接着讨论了安全性水平仅仅是描述攻击者所面临的潜在搜索空间。
我详细介绍了基于流行的 AES 算法和常见操作模式的对称加密领域,因为它在安全性中无处不在。像 SHA-256 这样的哈希函数、用于完整性保护的 MAC 算法,以及像 AES-GCM 这样的高效 AE 方案,完善了现代对称加密工具箱。
基于 RSA 和 DH 的非对称加密的引入,极大地拓展了保护通信和设备的可能性:公钥加密、数字签名以及通过不安全通道的安全密钥协商。此外,ECC 领域显著提升了这些机制的性能。
后量子加密 超出了本书的范围,但它可能是未来挑战现有非对称加密方案的一个话题,且如果建成通用量子计算机,这一挑战将是不可避免的。NIST 对后量子加密的标准化进程(*csrc.nist.gov/projects/post-quantum-cryptography)仍在进行中,但值得关注。
如果你有兴趣深入了解加密学的内部工作,Jean-Philippe Aumasson 的 Serious Cryptography(No Starch Press,2017;第二版即将发布)和 Christof Paar 与 Jan Pelzl 的 Understanding Cryptography(Springer,2009)是很好的参考书籍。
第二部分:设备安全构建模块
如果你想设计一辆车,显然你需要一个发动机和车身,一些轮胎和座椅,可能还需要至少一扇窗户。如果你打算设计一个安全的设备,你同样需要一些基本的构建模块。
现代安全性如果没有可靠的随机数源和为所选处理器优化的加密操作是无法实现的。此外,由于许多加密算法依赖于秘密,因此需要一个可靠的存储选项来保护这些机密数据。最后但同样重要的是,唯一且安全的设备身份与安全的通信通道必须是每个联网设备的必备条件。由于这些话题的细节非常关键,本书的第二部分为每个话题专门分配了一章。
第三章:随机数生成器(RNG)**

随机性是决定论的自然对立面。虽然我们希望大多数设备功能遵循后者,但随机数生成器(RNGs)的目的是提取前者。但在集成电路(IC)中产生随机数是否可能?如果可以,我们是否能够用实际需求来描述“良好”的随机性?
有些人认为,真正的随机性只存在于量子力学过程之中(他们可能是对的),但量子实验通常难以在标准互补金属氧化物半导体(CMOS)芯片中进行。另一方面,许多安全应用程序绝对需要一个随机数源才能达到预期的保护级别。
本章首先讨论了要求随机性才能被认为安全的应用程序。它介绍了在微芯片中提取随机性的常见方法以及真正的随机性背后的思想。此外,我还解释了伪随机性的概念,以及为什么它对于实际场景是必要的。最后,我提供了三种简单的工具,可以帮助你评估随机数集,进而发现可能的实现缺陷。
随机性的重要性
许多安全概念依赖于随机性。例如,当生成密码、加密密钥或唯一令牌等机密信息时,这些数据只能为特定实体所拥有。自然,这个生成过程的结果必须是完全不可预测的。否则,机密信息的搜索空间将变小,从而降低安全级别,使得攻击变得更加高效,正如第二章中所讨论的那样。
挑战-响应认证是许多流行安全协议的一部分,它是另一个随机性至关重要的应用场景。在这种认证方式中,验证者向声明给定身份的实体发送一个随机且不可预测的挑战,以启动认证过程。然后,这个随机值必须与一个独特的秘密一起处理,以证明声明的身份。然而,如果挑战是可以提前知道的或可能是重复的,攻击者将能够为这种情况做好准备,并通过其他方式获得预期的响应。
由于秘密密钥和安全通信对于许多物联网设备至关重要,因此这些设备中对随机数生成器(RNG)的需求非常明显。然而,在大多数情况下,软件解决方案无法解决这个需求,因此它必须集成到硬件中,通常是设备的主处理器或专用安全芯片中。因此,在考虑设备安全时,RNG 的需求应成为每次处理器采购过程的一部分。
注意
在实际操作中,一些软件库(如 Mbed TLS)明确要求你定义一个用于安全操作的随机性来源。如果没有这个来源,它们将无法工作。
随机性的本质
尽管随机性是自然存在的事物,但却很难描述。一个简单的解释是,随机性来自于一个实验,该实验的结果在执行之前无法预测,就像掷硬币一样。即使这个实验已经执行了百万次,我们也无法在结果中找到任何模式或特征的重复,除了纯粹猜测外,无法推导出关于输出的任何确定性。
这种不确定性和不可预测性的衡量标准被称为熵。它也被用于其他学科,如物理学或信息理论,用于描述系统或数据中包含的无序或信息量。在 RNG 上下文中,熵描述了可以被认为是真正随机的比特数。一个不好的 RNG 可能每个字节只产生 2.4 位熵,这意味着关于单个 RNG 输出字节的 5 位以上的信息可能会以较高的确定性被猜测出来。
到这一点,可能已经很清楚,我们期望每个字节有 8 位熵用于高质量的随机数生成器(RNG)。然而,熵本身或其缺乏很难衡量,因此,RNG 专家依赖于对大量收集的随机比特的统计评估,以识别缺陷并评估其设计优化。这些统计要求中的两个是独立性和均匀分布。
独立性意味着每个随机性生成实验应该与之前运行的实验相互独立。因此,输出比特的值没有条件发生概率,因此与之前提取的输出比特没有任何关系。图 3-1 展示了四个二进制数据集。

图 3-1:可能包含模式的随机比特的二维可视化
在图 3-1a 和图 3-1d 中,我们的大脑会立即检测到模式,这意味着二进制数据存在重复。这些显然比其他的更不随机。仔细观察,图 3-1c 展示了一个微妙的模式,而图 3-1b 则没有显示出任何可视的比特之间的关系。
我们还可以说,随机比特的均匀分布要求每个 RNG 能生成的符号的概率必须相等。图 3-2 展示了四种符号分布生成的随机比特。

图 3-2:可能包含偏差的随机比特的二维可视化
用肉眼可以看到,图 3-2b 和图 3-2c 包含的白色方块比黑色方块更多。具体来说,图 3-2b 中 70%的区域是白色的,而图 3-2c 中 80%是白色的。区分图 3-2a 和图 3-2d 更难,但通过观察数字,情况变得清晰。图 3-2a 的黑白比例为 45:55,而图 3-2d 则是完全的 50:50。
真随机数生成器
真随机数生成器 (TRNG),有时也叫做非确定性随机比特生成器 (NRBG),其主要任务就是提取“真正”的随机性。我们可以实现一个熵源,如 NIST 的特别出版物 800-90B中所描述,进行微观尺度的实验,并将其结果作为随机比特输出,从而实现真正的随机性。形象地说,可以想象一个纳米级的人定期掷硬币,并根据正反面结果控制晶体管。
熵的物理来源可以是半透明镜面上的光子传输、放射性原子衰变过程的观察,甚至是熔岩灯的墙面(blog.cloudflare.com/randomness-101-lavarand-in-production)。然而,这些想法在 CMOS 设计中实现起来都比较困难,这意味着芯片制造商通常依赖于集成电路中与热噪声和电子噪声相关的物理效应。
环形振荡器
提取和积累这种噪声的常见电路是环形振荡器 (RO)。它由奇数个反向器组成,按环形结构连接。开机时,门电路将它们的输出驱动到输入信号的反向电平。图 3-3 展示了一个三反向器的 RO 电路。

图 3-3:一个基本的 RO 电路
假设最左边的反向器输入为高,其输出将为低,第二个反向器的输出将为高,最后的输出信号再次为低。将最终输出反馈到第一个输入端会导致不匹配,从而导致所有反向器相应地改变输出。如你所见,这个稳定的电路将永远无法静止,而是会在某个频率上振荡。
在一个完美的世界中,RO 输出信号将是完全确定性的,其振荡频率也是如此。然而,在现实中,信号的行为取决于实现的物理特性以及其所处的物理状态。这两个因素都会导致 RO 信号的抖动,进而导致频率变化,有时略高,有时略低。简而言之,信号及其频率容易受到噪声的影响。尽管我们仍然能够部分预测 RO 的行为,但一些特性确实源自随机性。在许多情况下,这是微芯片中“真正”随机性的第一步。
由于单一振荡器提供的熵量仅为“少量”,因此通常会将多个振荡器组合在一起形成 TRNG。图 3-4 展示了这些振荡器信号如何通过触发器进行采样,经过 XOR 操作组合后,再次进行采样,以获得一个具有高熵的单一随机位。

图 3-4:使用多个 RO 的基本 TRNG 架构
然而,在商业设备中,我们几乎无法表征这些内部设计决策及其电路的实际熵。我们必须信任芯片制造商。前面的段落只是想让你了解“真正”随机性可能来自哪里。
熵源的健康状态
熵源的健康状态应该是可用的,以便 TRNG 用户能够在熵源失效时通知系统。否则,设备可能会依赖于实际上全是零的随机数。因此,对于高质量的 TRNG,至少需要有三种测试程序来监控其熵源的正确功能:
启动测试 一些熵源必须成功地完成初始化阶段,才能生成高质量的随机位。例如,对于基于振荡器的电路,预期的振荡必须启动,而不能卡在一个非振荡状态。因此,TRNG 必须实施启动测试,仔细监控其熵源的启动阶段,并在失败时引发错误。
完全失败测试 环境因素,如温度变化、电源电压变化或有意攻击,可能导致熵源功能的突然丧失。完全失败测试监控熵源的输出位,检测这些事件并在发生时发出警报。
在线测试 虽然前两个测试监控熵源的完全停机,在线测试则持续观察生成的随机位的分布,以识别漂移和偏差。如果它们超过一定限度,这也可能是通知操作系统或应用程序的原因。
尽管 TRNGs 是唯一提取“真实”随机性的方式,并且对安全设备至关重要,但它们也有缺点。它们依赖于物理过程,这些过程自然受环境情况的影响,这可能导致行为变化甚至错误。此外,提取和收集原始熵位需要时间,通常无法满足高性能的要求。下一节将介绍缺乏“真实”随机性但表现更好的原语。
伪随机数生成器
伪随机数生成器(PRNGs) 与 TRNGs 正好相反,也被称为确定性随机位生成器(DRBGs),因为与 TRNGs 不同,它们的行为是完全确定的,并且不包含熵源。这两种特性是 PRNGs 的优点,因为它们使得 PRNGs 可靠且能抵御环境影响,同时也允许实现高速操作。
PRNG 算法的关键要求是回溯抗性和预测抗性。这两个要求都假设攻击者已经访问了 PRNG 的当前状态(输出)。前一个要求要求对手无法从这些信息中推导出任何以前的状态值,而后一个要求则要求攻击者无法预测 PRNG 算法的未来输出值。这些要求可能让你想起哈希函数是单向函数,或者块密码加密在没有相应密钥的情况下无法被反转——你是对的。这些算法非常适合当前任务。
在《特别出版物 800-90A》中,NIST 的专家推荐了三种 PRNG 构造。Hash_DRBG使用一个经过批准的哈希函数,这个哈希函数适用于所需的安全级别,用于处理初始和中间状态数据,以推导出伪随机输出位并获取后续状态。HMAC_DRBG和CTR_DRBG分别使用带密钥的哈希函数和计数器模式下的块密码,其中相应的密钥不是固定的,而是作为 PRNG 操作的一部分定期更新。
也许你听说过线性反馈移位寄存器(LFSRs)和梅森旋转算法(Mersenne Twister)PRNGs,并想知道为什么它们虽然效率高,但并不推荐使用。它们确实产生看似随机的序列,但它们在加密安全性上存在问题,因为它们的架构仅依赖于线性组合,而这种组合无法提供预测抗性。简单来说,任何能够观察到一定数量输出值的攻击者,都能够推导出未来的值。
注意
你可能会偶然发现 Dual_EC_DRBG,一个原本设计为加密安全的伪随机数生成器(PRNG)。然而,它包含了一个潜在的后门,可能是故意插入的。请谨慎选择你的加密原语!
除了为 PRNG 算法做出稳妥选择之外,种子是 PRNG 使用中最重要的部分之一。通过种子,PRNG 使用具有足够熵的数据来初始化,以实现所需的安全目标。例如,如果需要 128 位安全级别,则 PRNG 必须用至少 128 位熵的比特串进行初始化。此外,这些信息必须视为机密,绝不能向攻击者透露。
可选地,我们可以实现定期的 重新种子,即更新 PRNG 实例的熵池,添加“新鲜”的熵。此外,我们可以通过添加唯一值,如设备序列号、随机数或时间戳,来个性化 PRNG 的初始化。然而,需要小心,因为这些附加数据始终被认为是 没有熵的,因为它可以在不泄露安全性的情况下公开获得。
实用的 RNG 构造与使用
在阅读了前一章后,你可能已经猜到,结合 TRNG 和 PRNG,你可以为安全设备实现最佳的 RNG 解决方案。前者持续(并缓慢)提取熵,这些熵用于为 PRNG 提供种子,并随后重新种子 PRNG,后者可靠地为操作系统和应用程序提供几乎任意数量的随机位。
如果你的设备运行操作系统,那么操作系统开发人员很可能已经处理了其中的难点。但即便如此,架构师和开发人员仍需要牢记以下话题。
RNG 选择
即使操作系统提供了强大的 RNG,并非所有框架和编程语言默认使用它。通常,必须明确选择它,或者在代码中使用相应的 API。否则,你将依赖于非加密的 PRNG,这可能会破坏设备的安全性。
错误处理
如前所述,TRNG 可能会失败。此外,操作系统提供的 RNG 也可能会返回错误,因此请确保认真对待这些返回值并进行相应的处理。你最不希望发生的情况是,设备使用的看似随机数据实际上只有零,而没有人注意到。
启动时熵
一些过程在启动过程中或紧接着启动后进行,例如设备初次启动时的安全 Shell(SSH)密钥生成。然而,在启动时,设备可能还没有收集到很多熵,甚至可能没有任何熵。反过来,这可能会导致多个设备上的密钥相同或至少相似,这是绝对不希望发生的。请记住,关键的密钥生成过程需要等到收集到足够的熵后再进行。
案例研究:从硬件到 Python 的随机数生成
在本案例研究中,我分析了 STM32MP157F 设备的硬件 RNG 特性,解释了 Linux RNG 的工作原理,并指出为什么在 Python 中仍然需要小心使用正确的 RNG。
硬件 RNG 和熵源
查看 ST 的参考手册 RM0436,你可以获得关于 STM32MP157F 设备集成 RNG 的大量信息。在对应章节的开始部分,ST 写道,实施的 RNG 可以作为构建符合 NIST 标准的 DRBG 的基础,并且它已通过德国联邦信息安全办公室(BSI)的 AIS-32 测试套件成功测试。
警告
请小心,因为实现的 RNG 本身并不等同于 NIST DRBG。参考手册的后面部分指出,如果需要具有 128 位安全级别的符合 NIST 标准的 DRBG,则必须在此 TRNG 之上添加一个经过批准的 PRNG。
仔细阅读手册,你会发现实现的 TRNG 使用了两个模拟噪声源,每个噪声源包含三个自由运行的 RO,这些 RO 通过 XOR 运算来混合它们的输出。然后,XOR 结果通过一个专用时钟信号进行采样,并经过后处理以去除原始位可能存在的偏差。此外,一个条件阶段“提高了熵率”,但该过程没有详细描述。最终输出包含 128 位的随机数据,通过一个先进的 32 位高性能总线(AHB)由一个先进先出(FIFO)缓冲区提供。
关于噪声源的持续监控,ST 实现了重复计数测试。例如,如果某个噪声源输出超过 64 位相同值或 32 次重复相同的 2 位模式,则会触发错误。可以通过 AHB 访问的状态寄存器显示发生的错误。
到目前为止的好消息是,所选的微芯片提供了硬件 RNG。像往常一样,评估这个模块的质量对于用户来说是困难的,但如果我们相信制造商及其安全能力,这个硬件 RNG 可以是一个有价值的资产。
Linux 中硬件 RNG 的集成
在 Linux 中,硬件 RNG 可以通过hw_random框架进行集成,该框架由硬件特定的驱动程序和一个通用的内核接口组成,从而创建对应的/dev/hwrng设备。如果在 Linux 内核配置中启用了CONFIG_HW_RANDOM并且供应商提供的驱动程序正常工作,你可以检查硬件 RNG 的可用性和选择,正如我在列表 3-1 中为我的 STM32MP157F 设备所示。
# cat /sys/class/misc/hw_random/rng_available
optee-rng
# cat /sys/class/misc/hw_random/rng_current
optee-rng
列表 3-1:检查硬件 RNG 的可用性
此外,如果在 Linux 系统上安装了rng-tools,你可以像列表 3-2 中所示一样,对集成的硬件 RNG 进行统计测试。
# rngtest -c 1000 < /dev/hwrng
rngtest 6.15
...
rngtest: starting FIPS tests...
rngtest: bits received from input: 20000032
rngtest: FIPS 140-2 successes: 999
rngtest: FIPS 140-2 failures: 1
rngtest: FIPS 140-2(2001-10-10) Monobit: 0
rngtest: FIPS 140-2(2001-10-10) Poker: 0
rngtest: FIPS 140-2(2001-10-10) Runs: 0
rngtest: FIPS 140-2(2001-10-10) Long run: 1
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=117.995; avg=137.966; max=149.035)Kibits/s
rngtest: FIPS tests speed: (min=14.638; avg=28.942; max=29.434)Mibits/s
rngtest: Program run time: 142228806 microseconds
列表 3-2:对来自 RNG 硬件设备的数据进行统计测试
这些测试源自 NIST 的FIPS 140-2加密要求文档。这里的数学细节并不重要,而且少量的失败并不值得担心。请注意,这个硬件 RNG 源以大约 138Kb 每秒的速率提供随机数。
Linux RNG 架构
像其他大型操作系统一样,Linux 也有自己的随机数生成器(RNG)概念和伪随机数生成器(PRNG)实现。它最早在 1994 年引入,当时其架构基于 SHA-1 操作,因为强加密算法是美国出口限制的一部分。
然而,从 Linux 内核 5.17 版本开始,SHA-1 已完全从代码中移除。PRNG 实例现在依赖于 ChaCha20 密码算法,而熵池的压缩功能则通过BLAKE2s的哈希更新操作实现,BLAKE2s 也是基于 ChaCha 的哈希函数。5.18 版本引入了 Linux RNG 的各种附加改进(例如,关于启动时熵的改进)。
熵池是一个内部的 256 位内存缓冲区,它从一组噪声源收集数据,并用于为 ChaCha20 PRNG 的基础实例提供数据。如果在给定平台上有硬件 RNG 可用,正如我们在 STM32MP157F 设备上所看到的,它提供的熵可以通过add_hwgenerator_randomness接口集成到熵池中。其熵内容估算取决于驱动程序代码中提供的熵质量值。对于我的设备,该值为 900,这意味着每个 RNG 位的熵大约为 900/1,024 = 0.879 位。
进一步的噪声源可能有助于熵的收集。如果系统中有旋转硬盘或类似的块设备,add_disk_randomness可能会对你的熵池产生正面贡献。然而,由于我的 STM32MP157F-DK2 开发板运行在 microSD 卡上,这个机制没有任何贡献。此外,按键和鼠标移动可以用来提取一定量的噪声,并通过add_input_randomness增强内部池,但在嵌入式系统使用中,用户交互通常很少。另外,设备驱动程序可以提供在设备初始化期间可能可用的随机数据,通过add_device_randomness将其集成到熵池中。然而,默认情况下它被认为是零熵。
proc文件系统提供有关已实例化熵池的信息,例如其大小和当前的熵估计水平(以位为单位),如列表 3-3 所示。
# cat /proc/sys/kernel/random/poolsize
256
# cat /proc/sys/kernel/random/entropy_avail
256
列表 3-3:打印熵池信息
请记住,较旧的 Linux 系统基于另一种 RNG 架构,在这个时候它会显示 4,096 位的池大小,而不是 256 位。
在相同的路径下,文件bootid提供了一个唯一的、随机的标识符,用于当前的运行时,如列表 3-4 所示。它会在下次启动时发生变化。
# cat /proc/sys/kernel/random/boot_id
e67a7d3e-3825-4019-ad86-940a7c8748df
列表 3-4:显示启动识别号
相比之下,列表 3-5 显示了读取uuid会为每次访问生成一个新的全局唯一标识符(UUID)。
# cat /proc/sys/kernel/random/uuid
703a31fe-fd53-44d4-8a85-075416a107ea
# cat /proc/sys/kernel/random/uuid
10f38bb8-66a8-4951-b9b4-db89db438ba8
# cat /proc/sys/kernel/random/uuid
6abf0dd4-2d0b-421b-b04e-8f02f994149b
列表 3-5:从 Linux RNG 读取 UUID
从用户空间,可以通过多种方式获取随机数:
从 /dev/random 读取 设备文件 /dev/random 的行为是阻塞的。这意味着,如果 ChaCha20 PRNG 尚未接收到 256 位的熵,它将不会返回任何数据。
从 /dev/urandom 读取 /dev/urandom 的非阻塞特性允许无论初始熵收集状态如何,都能从该设备读取随机数,这使得它成为开发人员的热门选择。
使用 getrandom 系统调用 该系统调用可以通过参数化来定义其行为。将标志设置为零时,它的行为与 /dev/random 一样,但如果将标志设置为 GRND_INSECURE,它将像 /dev/urandom 一样返回随机数。
在内核空间中,可以使用 get_random_bytes 函数访问 PRNG 实例并从中获取随机数。此函数不依赖于 ChaCha20 PRNG 初始化和熵池的状态。
在 列表 3-6 中展示了一个简单的单行代码,将 1MB 随机数据写入文件。
# dd if=/dev/urandom of=rand_file.bin bs=1M count=1 iflag=fullblock
1+0 records in
1+0 records out
1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.0638873 s, 16.4 MB/s
# ls -l rand_file.bin
-rw-r--r-- 1 root root 1048576 ... rand_file.bin
列表 3-6:生成一个包含 1,048,576 个随机字节的文件
如 列表 3-7 所示,使用 /dev/urandom 调用 rngtest 可以实际演示将硬件随机源与可靠的软件 PRNG 结合的优势。
# rngtest -c 1000 < /dev/urandom
rngtest 6.15
...
rngtest: starting FIPS tests...
rngtest: bits received from input: 20000032
rngtest: FIPS 140-2 successes: 999
rngtest: FIPS 140-2 failures: 1
rngtest: FIPS 140-2(2001-10-10) Monobit: 0
rngtest: FIPS 140-2(2001-10-10) Poker: 0
rngtest: FIPS 140-2(2001-10-10) Runs: 0
rngtest: FIPS 140-2(2001-10-10) Long run: 1
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=85.917; avg=185.867; max=190.735)Mibits/s
rngtest: FIPS tests speed: (min=12.203; avg=28.918; max=29.756)Mibits/s
rngtest: Program run time: 763735 microseconds
列表 3-7:在 /dev/urandom 上运行 RNG 测试
统计数据仍然正常,但速度已提高到大约 186Mbps。与原始硬件 RNG 相比,这提高了超过 1,000 倍。
Python 中的密码学安全随机数
即使我们有一个集成了硬件 RNG 的设备,且 Linux 的 RNG 行为正常,我们仍然可能在应用程序级别无法实现密码学安全的随机数生成。在这个案例研究中,我们将查看 Python,但许多其他编程语言和环境也面临同样的问题。
Python 教程通常使用 random 模块进行随机数生成。然而,它的文档明确指出,这是基于梅森旋转算法的 PRNG,旨在进行建模和仿真等用途,但不适合用于密码学应用。
要使用操作系统提供的 RNG,os 模块提供了 os.urandom() 函数。然而,在 Python 3.6 或更高版本中,推荐的方式是使用 secrets 模块,它专门用于提取密码学安全的随机数,并选择系统中最安全的随机源。
列表 3-8 显示了一个脚本,它从命令行参数中获取字节数和文件名,使用 secrets 模块的 token_bytes() 函数生成相应数量的随机字节,并将其写入指定的输出文件。
import sys
import secrets
n_bytes = int(sys.argv[1])
output_file = sys.argv[2]
print('Generating', str(n_bytes), 'random bytes ...')
random_bytes = secrets.token_bytes(n_bytes)
print('Writing random bytes to', output_file, '...')
with open(output_file, "wb") as f:
f.write(random_bytes)
列表 3-8:使用 Python 的 secrets 模块创建一个包含随机字节的文件
最后,这个案例研究展示了在为加密目的生成随机数时,需要考虑硬件和软件中的不同层次。
案例研究:随机性快速检查的实用工具
如果你负责开发和测试新的 RNG 设计,你将希望使用各种统计分析算法,如 NIST 的 Special Publication 800-22 中描述的,或由 Dieharder 套件提供的,但实际上,地球上只有极少数人会设计新的 RNG。
然而,许多人使用和处理随机数,这可能会导致实际实现错误,尤其是在应用层面。一些错误可以通过使用相对简单的工具来发现。本案例研究解释了两种这样的方式,并展示了如何使用它们发现常见问题。
分布分析和模式识别的简单工具
本章开始时,我们看到偏差(或任何形式的不均匀分布)和重复模式是低质量随机数的两个指标。本节介绍了一种简单的方法来分析基本数据特征,但这些远远没有达到专业统计分析的水平。它们仅有一个目的:为随机数生成过程中的问题提供线索,最可能出现在最后阶段(即在应用程序中)。
列表 3-9 显示了一个简短的脚本,它将文件作为命令行参数,读取文件并绘制直方图,显示所有可能字节值的出现频率。在完美的情况下,直方图应该显示为一条平坦的线,意味着文件中所有字节的出现概率是相等的。
import sys
import matplotlib.pyplot as plt
file_to_analyze = sys.argv[1]
print('Reading', file_to_analyze, '...')
with open(file_to_analyze, "rb") as f:
data = f.read()
hist_data = bytearray(data)
print('Plotting distribution of bytes ...')
fig1, ax1 = plt.subplots(figsize=(15, 5))
ax1.hist(hist_data, bins=range(256+1), align='left', color = "gray")
ax1.set_title('Distribution of Bytes')
ax1.set_xlabel('byte value')
ax1.set_xticks([0, 32, 64, 96, 128, 160, 192, 224, 256])
ax1.set_ylabel('frequency of occurrence')
plt.show()
列表 3-9:一个 Python 脚本,用于绘制给定文件的字节分布
为了说明,我使用 Linux RNG 生成了一个包含 1MB 随机数据的文件,并对其运行了该脚本。图 3-5 显示了相应的分布情况。它不是一条平坦的线,但各个频率彼此非常接近。

图 3-5:来自 Linux RNG 的字节值分布
模式检测比偏差分析更复杂,因为模式可能有多种类型。然而,一些应用程序正是为这个任务优化的:压缩工具。这些算法会遍历数据,尝试找到重复的序列,建立字典并去除冗余。应用于随机数据时,每一次压缩成功意味着数据集存在熵缺失。虽然压缩工具并不会告诉我们具体问题是什么,但它们可以明确指出文件的信息内容低于从随机数据中预期的水平。
列表 3-10 显示了一个简单的脚本,使用常见的 Linux 压缩工具 bzip2、gzip 和 xz 对一个假设包含随机数据的文件进行操作。
import sys
import subprocess
import os
file_to_analyze = sys.argv[1]
print('Compressing', file_to_analyze, '...')
run = subprocess.run('bzip2 -k ' + file_to_analyze, shell=True)
run = subprocess.run('gzip -k ' + file_to_analyze, shell=True)
run = subprocess.run('xz -k ' + file_to_analyze, shell=True)
print('Compression test results:')
print('{:<28}'.format(file_to_analyze), '-->',
'{:>9}'.format(os.path.getsize(file_to_analyze)), 'bytes')
print('{:<28}'.format(file_to_analyze + '.bz2'), '-->',
'{:>9}'.format(os.path.getsize(file_to_analyze + '.bz2')), 'bytes')
print('{:<28}'.format(file_to_analyze + '.gz'), '-->',
'{:>9}'.format(os.path.getsize(file_to_analyze + '.gz')), 'bytes')
print('{:<28}'.format(file_to_analyze + '.xz'), '-->',
'{:>9}'.format(os.path.getsize(file_to_analyze + '.xz')), 'bytes')
print('')
列表 3-10:一个 Python 脚本,用于使用典型压缩工具测试随机数据
再次,来自 Linux RNG 的 1MB 数据作为一个正面示例。在 列表 3-11 中显示,所有的压缩工具都生成了比原文件更大的文件。这是预期的,因为内容无法压缩,并且压缩格式的头信息也被添加到文件中。
data_urandom.rnd --> 1048576 bytes
data_urandom.rnd.bz2 --> 1053538 bytes
data_urandom.rnd.gz --> 1048771 bytes
data_urandom.rnd.xz --> 1048688 bytes
列表 3-11:对 Linux RNG 数据进行压缩测试的结果
问题 1:通过模运算限制输出空间
在某些情况下,可能需要限制随机变量的输出空间,例如限制在 0 到 99 的范围内,因为某些应用程序要求这个范围。限制整数运算结果的一个简单方法是使用模运算符。为了获得一个 0 到 99 之间的整数,我们可以将所有的随机数取模 100。
为了测试这个情况,我在 列表 3-8 中的 Python 示例中添加了这个操作。图 3-6 显示了字节分布的结果。

图 3-6:限制模运算时字节的分布
虽然我们期望字节值达到 100 或更高时概率会下降至 0,但在值为 56 时却出现了一个奇怪的概率下降。然而,这意味着该应用程序将生成从 0 到 55 的值,其概率明显更高。从安全角度来看,这是不可接受的。
对于 Python,一个稳健的解决方案是使用 secrets 模块中的 randbelow() 函数,以获得在一定整数范围内均匀分布且加密安全的随机数。
问题 2:自定义 PRNG 设计
有时我们不得不依赖一些“黑箱”应用程序。我们无法评估软件的内部实现,更不用说随机数处理的质量了。然而,很有可能一个应用程序会实例化它自己的 PRNG,可能是出于性能考虑或使用便利性。也许它甚至由操作系统提供的强随机数生成器(RNG)来做种子。然而,如果自定义的 PRNG 设计不合理,可能会带来安全问题。
对于这个测试用例,我故意实现了一个弱的 PRNG。它或多或少是一个经过修改的 16 位计数器,由 Linux RNG 提供种子。它并不是非常合理,但也并非完全不现实;开发人员可以富有创造力。像第一次的情况一样,我从这个 PRNG 实例中提取了 1MB 的测试数据,并绘制了其字节分布,如 图 3-7 所示。结果显示出完美的分布。虽然这可能已经引起怀疑,但它并不直接表明有弱点。

图 3-7:自定义 PRNG 的字节分布
然而,在应用前面解释的压缩测试后,很明显存在问题。列表 3-12 显示了输出结果。
data_custom_prng.rnd --> 1048576 bytes
data_custom_prng.rnd.bz2 --> 86756 bytes
data_custom_prng.rnd.gz --> 997143 bytes
data_custom_prng.rnd.xz --> 84648 bytes
列表 3-12:对来自自定义 PRNG 的数据进行压缩测试的结果
三种压缩工具中有两种将文件大小缩小了超过 12 倍。这个变化是显著的。它虽然不能告诉我们当前应用程序的具体问题,但它引发了与开发人员的讨论。
总结
随机性是一个复杂的概念。然而,阅读完这一章后,应该能清楚地认识到随机性对安全性至关重要,且在硬件和软件组件的处理上需要格外谨慎。
我解释了 CMOS 微芯片中熵源的一些基础知识,以及它们如何从噪声中生成“真正”的随机性——例如,振荡电路中的噪声。此外,我讨论了伪随机数生成器(PRNGs)的必要性及其可靠的行为和高速数据传输等优点。真实随机数生成器(TRNGs)和伪随机数生成器(PRNGs)都是相关的,应该结合使用,以构建适用于嵌入式系统的安全可靠的随机数生成器(RNG)。
在第一个案例研究中,我深入分析了一个真实系统中的随机性层次——从 STM32MP157F 设备中的集成物理噪声源开始,到它们在 Linux 内核中的集成,再到 Linux RNG 本身的复杂架构。最后,我补充道,即使在应用层,若处理不当 RNG 及其数据,也可能导致安全漏洞。如果你对 Linux RNG 及其随时间演变的详细信息感兴趣,请参考德国 BSI 定期发布的 Linux RNG 分析文档。
本章通过第二个案例研究,介绍了一些简单且务实的工具,用于识别 RNG 及其输出数据中的问题,尤其是在第三方软件中的问题。
第四章:加密实现

在第二章中,我概述了加密算法、它们的参数以及典型的应用场景。然而,数学算法离安全高效的加密实现还有很长的路要走。
在大多数应用中,加密并不起主导作用。它更像是保护设备和业务资产的必要“恶事”。因此,开发人员和产品经理都希望有能够快速运行、不占内存、不消耗能量的加密实现。当然,这是不可能的,但在很多情况下这是一个重要的讨论点。低效的实现可能会导致安全功能被排除、产品质量下降,或者至少让那些不专注于安全的同事产生抱怨。
在本章中,我将讨论加密实现的需求以及开发人员的选择选项。接下来的部分介绍了对称加密和非对称加密算法优化的示例。最后的案例研究分析并讨论了在 STM32MP157F 设备上硬件和软件中加密实现的性能特征。
实现上下文和需求
开发人员在产品设计和架构开发的早期阶段选择中央微芯片,因为许多后续决策都依赖于此。该微芯片可能是单核微控制器、同质或异质多核系统、现场可编程门阵列(FPGA),甚至是将处理器、外设以及可能的 FPGA 整合到一个单一封装中的系统单芯片(SoC)。
如果选择了典型的处理器,几个参数会影响其整体性能,包括加密性能。这些参数从指令集架构(ISA, 例如 ARM、RISC-V 或 MIPS)和数据宽度(8 位、16 位、32 位、64 位甚至更高)开始。核心数及其最大频率也非常重要。特别是在加密操作中,值得关注的是是否有专门的加密指令,比如英特尔的 AES 新指令(AES-NI)扩展,或者给定的芯片是否配备加密协处理器。
对于工业、汽车或数据中心应用,考虑包括 FPGA 部件的微芯片可能是有趣的选择,因为它可以利用数字硬件设计所能提供的高性能属性或实时保证。除了最大支持频率,FPGA 还有一些特性,比如查找表的数量、触发器、随机存取内存(RAM)块、乘法器等,这些特性可能会设置加密实现的限制。
无论选择哪种类型的处理单元,都需要内部和/或外部内存。通常,需要易失性内存(如 RAM)和非易失性内存(如闪存、存储卡或固态硬盘(SSD))。除了它们的大小(这只会在非常资源受限的设备上影响加密实现)之外,它们的读写速度可能会影响加密应用程序的整体性能。
最后但同样重要的是,设备的有线和无线接口的传输速度(例如 Wi-Fi、蓝牙、以太网和专有总线)限制了通信的带宽,包括加密保护通道。
注意
在实际操作中,当讨论加密性能时,设备和硬件架构往往已经或多或少固定,你必须尽力做到最好。然而,不要害怕在早期阶段提出加密性能要求。例如,如果你的预期应用程序要求每秒处理数千个签名,那么这一要求必须在硬件选择时予以考虑。
从应用侧来看,你可能会对加密实现提出几种类型的需求。延迟描述从输入到输出处理单个数据块的时间,而吞吐量定义了在给定时间内可以处理的数据量。
对于资源受限设备上的软件实现,可能需要限制编译后的二进制文件的静态代码大小,这些文件必须存储在易失性和/或非易失性内存中。此外,运行时的动态内存使用也是一个相关因素。另一方面,FPGA 实现需要静态实例化数字组件,这也是为什么除了性能之外,数字硬件设计在加密领域的效率通常通过占用的 FPGA 资源数量进行比较的原因。
由于一些物联网设备由电池供电,甚至有些依赖于能量收集,因此加密实现的能耗也可能成为一个有效的需求。
设备实现上下文的所有信息对于做出稳健而高效的加密决策都是至关重要的。在某些情况下,在资源消耗和安全性之间找到合理的折衷可能是必要的。
选择加密实现
在过去几十年里,我们看到许多实现问题导致了漏洞。如果你不想自己经历这些宝贵但令人疲惫的经验,可以看看现有的优化和成熟的加密库,比如 OpenSSL、LibreTLS、Mbed TLS 和 wolfSSL,仅举几例来自嵌入式系统领域的库。
然而,这个问题引出了许多开发人员在开发过程中会面临的一个问题:如何为设备或应用选择特定的加密库?这个决定在很大程度上取决于您的具体需求(如果已经明确提出)以及您所使用的框架和编程语言。以下列表描述了四种典型情况:
自由选择 有时仅规定了预期的保护目标——例如,通信应通过认证加密进行保护,而没有进一步的细节。这可能是公司内部功能的情况,且没有外部依赖。在这种情况下,可以考虑几种加密算法。例如,AES-GCM 和 ChaCha20-Poly1305 可能都是合适的选择。在这种情况下,对目标设备进行性能评估是有意义的。使用一组参数和多个加密库测试两种算法,以获得一个尽可能高效的解决方案。
强大的性能要求 在某些情况下,加密是必不可少的,并且必须满足高性能要求——例如,如果某个应用需要每秒生成数千个数字签名。由于 ECDSA 在签名生成上比 RSA 更快,因此该算法可能应该基于 ECC。然而,您仍然需要选择曲线类型。在给定硬件上测试来自可用加密库的一组曲线是选择最佳性能配置的有效方式。如果您的要求无法实现,可能需要更换硬件或实现定制优化的解决方案。
强大的资源限制 基于小型微控制器的设备在物联网中占据了很大一部分。然而,这些组件通常表现出明显低于基于 Linux 的设备的性能特征。尽管许多先进的加密算法可以在这些处理器上运行,但关于延迟、吞吐量、服务连接数等要求必须仔细选择。对目标硬件的实际评估至关重要。
固定算法和安全等级 如果算法和密钥大小预先固定(例如,由于兼容性问题),则范围会大大缩小。然而,如果特定实现没有固定,那么对不同加密库进行快速性能比较仍然具有价值,并可能解锁性能潜力。
尽管本章重点讨论性能,但延迟和吞吐量远不是加密实现中唯一需要考虑的质量特性。特别是对于安全应用,以下两种特性可能会产生显著的区别,即使它们导致较低的性能:
透明性和清晰性 可读、易懂且有文档记录的代码是非常棒的。它减少了错误、假设和误解。此外,这些特性使得开发人员及其产品更加值得信任。对于加密实现来说,这一点尤为重要。即使性能出色,意大利面条式的代码、极端优化导致代码完全无法理解的情况,以及结构破碎,可能仍然会使人忽视实现的优点。
支持与维护 开源软件促进了不可思议的项目开发。然而,一些项目没有良好的文档记录和支持。如果发现并报告了漏洞,可能没有人能立即修复它们。当然,商业软件和闭源软件也可能存在同样的情况。关键是,你需要关注特定库过去是如何维护的,以及是否有任何警告信号表明该库在不久的将来可能会失去维护,特别是对于加密库。
警告
除非你有非常充分的理由,否则不要自行开发加密实现!
无论你选择哪种加密算法,将来某一天由于量子计算或新的密码分析突破,可能会出现问题。因此,遵循加密灵活性的做法是有意义的,加密灵活性意味着算法(例如,分组密码)可以轻松地被另一种同类型算法替代。
AES 实现选项
在本节中,我想阐明在配置或分析 AES 实现时需要考虑的选项。这里以 AES 为例,但由于许多其他对称加密算法和哈希函数也具有基于轮次的结构和类似的操作,你也许能够将这些见解转化为其他算法。
基本架构
一个基本的考虑因素是处理数据的操作宽度。在最优的软件实现中,宽度通常会选择与底层硬件的数据宽度相匹配。然而,宽度不匹配可能会导致问题。例如,在 32 位中央处理单元(CPU)上进行 8 位实现可能会缺乏性能,而在 8 位 CPU 上进行 32 位实现可能无法编译,或导致低效的转换。
在 FPGA 实现中,实施的操作宽度可以随意选择。如果可以接受较慢的性能,并且重点是使用较少的资源,那么 8 位实现是合适的。另一方面,如果需要高性能,我们可能会希望使用 128 位的数据宽度进行操作。如果希望获得平衡的实现(即在所需资源和性能之间取得合理的权衡),32 位架构可能是一个可靠的选择。
AES 需要将给定的密钥扩展为一组轮密钥。可以在开始时进行一次密钥扩展,以提高批量数据加密和解密过程中的性能。另一种选择是在需要相应轮密钥进行操作时动态扩展。这样可以提高动态内存使用效率,因为只需要在内存中存储单个轮密钥,而不是所有轮密钥。
对于基于轮次的算法,如 AES,使用循环结构运行必要的轮次似乎很自然。然而,不断处理和检查循环变量会降低最大性能。即使比较和条件分支不会花费几秒钟,它们也会损害整体吞吐量。术语循环展开描述了一种你可能在高速实现中看到的方法:用一系列代码替代循环,表示所有 AES 轮次,以提高性能,但代价是增加了二进制文件的大小。
优化操作
AES S-盒通常表示为一个表,用于执行非线性替代操作。因此,它通常作为常量查找表在软件实现中进行实现。然而,这并不是唯一的选择。如果静态表不适合你的需求,你可以在 RAM 中动态生成替代表。另外,对于数字硬件设计,可以使用一种称为Canright S-盒的布尔电路实现。
实现 AES 的一种流行优化方法是将轮次操作 SubBytes()、ShiftRows() 和 MixColumns() 结合起来,以获得每轮每列四次表查找和四次 XOR 操作的高效序列,这也被称为T-表实现。它需要四个表,每个表包含 256 个 4 字节的单词,加起来加密和解密分别需要 4KB 的内存。进一步的优化可以将所需的表内存减少到 1KB,但代价是每列每轮增加三次旋转操作。
注意
牢记这些基本选项有助于你估算给定实现的性能特点,同时也可能在查看设备的性能数据时帮助你推导出其实现细节。
显然,提供专用 AES 加速指令或特定加密协处理器的平台可以进一步优化性能。一些加密库已经准备好支持这些优化——例如,流行的 AES-NI 指令。然而,在嵌入式系统中,通常需要付出一些努力才能在应用程序中利用硬件加速。
虽然性能提升无疑是使用硬件加密的最常见动机,但在某些情况下,它也可能改善设备的功耗,或者至少减轻主 CPU 的负担。
RSA 和 ECDSA 的实现特点
RSA 和 ECDSA 等非对称加密实现与对称加密实现不同。非对称加密算法不像对称加密那样具有一个填充变换集的循环结构,而是基于需要在大数上进行算术运算的数学问题。因此,这些算法在现实世界中的性能在一定程度上取决于 多精度算术(也称为 大数算术)的效率。
这些库需要克服的第一个障碍是一个简单的事实,即典型的处理器支持 32 位和 64 位数据操作,而 RSA 例如是基于 2,048 位、4,096 位甚至更多位长度的整数。这个问题可以通过将这些长整数拆分成一个 limb 数组来解决,通常每个 limb 的大小与 CPU 的最大数据宽度相同。
RSA 优化
尽管通用的大数库支持对大数字的全面算术操作,但加密算法通常只需要其中的一小部分。如 第二章 所示,RSA 的主要操作是模幂运算——例如,用于加密和签名验证:y = x^emod n。采用简单的方法,处理几千位的整数进行此计算几乎是不可能的。使用平方乘法算法使得这种计算成为可能。
RSA 有两个需要考虑的情况,它们具有非常不同的特性。首先,验证和加密操作使用公钥指数 e = 65537 = (10000000000000001)[2],这使得性能相当高。原因在于它的 17 位长度,二进制表示中有一个前导 1 和一个尾随 1,这导致根据平方乘法算法,只需要进行 16 次平方运算和一次乘法运算。正如你在本章稍后会看到的那样,这不仅比解密和签名功能(必须处理与 RSA 完整密钥长度相同的整数)要快得多,而且还超过了 ECDSA 验证速度。
RSA 的私钥操作无法以相同的方式进行优化,但一种叫做 中国剩余定理 (CRT) 的方法将运行时间大约缩短了 4 倍。之所以能够实现,是因为 CRT 利用了 n = pq,这使得它可以通过两个指数运算(分别对 p 和 q 取模)来获得 n 的指数运算结果。这种方法能够节省计算量,因为 p 和 q 的大小大约是 n 的一半。
RSA 性能的平方乘法算法中一个重要的方面是,其复杂度与处理的指数位长的立方成正比。当你从 2,048 位的 RSA 升级到更具未来性的 4,096 位 RSA 时,你会明显感受到这一关系的痛苦效果,因为密钥长度翻倍会导致运行时间大约增加 2³ = 8 倍,这可能会吞噬你所有的运行时间需求。
ECDSA 细节
由于 ECDSA 基于椭圆曲线运算,这些曲线处理的数字显著较小,因此私钥操作比 RSA 对应的操作要快得多。然而,不同类型的曲线支持不同的实现和优化。虽然选择合适的曲线涉及到数学和信任的考量,但在这个过程中性能不应被完全忽视,因为不同选项之间存在相当大的差异。
相较于对称加密,支持非对称加密的 FPGA 实现和内部硬件要少得多。你可以在一些专用的安全集成电路(IC)中找到它们,这些 IC 例如可以提供通过数字签名进行身份验证,也可以在支持数字签名验证的 SoC 设备中找到,以保护其启动过程。然而,通常你无法了解这些实现的太多细节,也无法任意访问它们的接口。
案例研究:STM32MP157F 设备上的加密性能
在这个案例研究中,我探讨了在 STM32MP157F 设备上运行的多种对称和非对称加密算法的性能,并讨论了你能从这些结果中学到的东西。由于嵌入式系统通常配备功能丰富的操作系统,因此在这些系统上进行高精度的性能测量非常困难。因此,所有结果应视为大致数据。
手头的 SoC 基于一颗 ARM Cortex-A7 双核处理器,运行频率最高可达 800 MHz,配备了两种类型的加密协处理器。CRYP1 核心支持 DES、Triple DES 和 AES 的不同操作模式。哈希模块 HASH1 提供 SHA-1、MD5、SHA-224、SHA-256 以及相应的 HMAC 操作加速。这两者的运行频率大约是 266 MHz。
在以下的测试案例中,使用 OpenSSL 命令行工具评估性能,因为它通常可以在 Linux 系统上使用,并且其软件实现已经高度优化,非常适合我们的任务。
调用 openssl speed -elapsed -evp 算法进行测试。-elapsed 选项定义了吞吐量是根据实际经过的墙钟时间计算的,而不是根据用户空间中的 CPU 时间。后者会扭曲结果,特别是在使用硬件支持时。-evp 标志表示 封装,启用一个通用的高级加密接口,视硬件可用性,既可以使用软件实现,也可以使用硬件实现。
结果控制台输出始终包括如 清单 4-1 所示的编译参数,以及以下一组输入数据大小的每秒字节数的测试结果:16 字节、64 字节、256 字节、1,024 字节、8,192 字节和 16,384 字节。
version: 3.0.5
...
options: bn(64,32)
compiler: arm-ostl-linux-gnueabi-gcc -mthumb -mfpu=neon-vfpv4
-mfloat-abi=hard -mcpu=cortex-a7 --sysroot=recipe-sysroot -O2 -pipe -g
-feliminate-unused-debug-types ...
-DOPENSSL_USE_NODELETE -DOPENSSL_PIC -DOPENSSL_BUILDING_OPENSSL -DNDEBUG
CPUINFO: OPENSSL_armcap=0x3
清单 4-1:给定 OpenSSL 工具的编译参数和元数据
为了清晰和易懂,所有测试的终端输出都简化为主要的相关数字。
对称加密的参数选择
让我们考虑一个用例,该用例要求对传感器值进行机密性保护,并且这些值应以 50KB 的块进行加密。假设你的团队成员已经选择了 AES 作为分组密码,但密钥大小和操作模式尚未确定。
第一次分析比较了 128、192 和 256 位密钥的 AES-CTR 模式的性能,以便对这些数字有一个直观的了解。列表 4-2 显示了在 STM32MP157F 设备上获得的结果:第一行显示了输入数据块的大小,第二行显示了与该特定输入数据大小相关的每秒千字节吞吐量。
# openssl speed -elapsed -evp aes-128-ctr
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-128-CTR 15102.09k 19198.53k ... 22915.75k 22943.06k
# openssl speed -elapsed -evp aes-192-ctr
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-192-CTR 13517.07k 16734.31k ... 19327.66k 19360.43k
# openssl speed -elapsed -evp aes-256-ctr
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-256-CTR 12158.66k 14689.77k ... 16711.68k 16728.06k
列表 4-2:根据 AES 密钥大小的性能差异
你可能注意到的第一个一般性点是,当输入数据大小增加时,吞吐量也会增加。这是因为必要的开销对于较大的输入数据的相关性减小。
对于我们的示例,最后一列是我们关注的,因为我们需要处理大约 50KB 的输入数据。在那里,我们可以看到 AES-128-CTR 达到了大约 22.9MBps,而 AES-256-CTR 仅达到了 16.7MBps。这是大约 27% 的性能下降,或者大约 37% 的处理时间增加。这是完全合理的,因为 AES-128 只需要计算 10 轮,而 AES-256 需要 14 轮,因此需要多 40%。然而,从安全性角度来看,我们可以获得 128 位的安全性,同时仅投资大约 37% 的性能。这次升级可能是值得的。
第二个有趣的点是操作模式对性能的影响。虽然 CTR、CBC 和 GCM 模式在安全性上有所不同,但它们的性能特点也不同。在列表 4-3 中,你可以看到 CTR 模式在较大输入数据时比 CBC 模式的性能高大约 8%,这可能是许多情况下选择 CTR 而非 CBC 的原因。
# openssl speed -elapsed -evp aes-256-ctr
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-256-CTR 12157.72k 14690.37k ... 16708.95k 16722.60k
# openssl speed -elapsed -evp aes-256-cbc
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-256-CBC 11962.59k 14429.91k ... 15515.65k 15515.65k
# openssl speed -elapsed -evp aes-256-gcm
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
AES-256-GCM 9052.80k 10844.44k ... 12126.89k 12113.24k
列表 4-3:AES 操作模式对加密吞吐量的影响
GCM 提供认证加密,这意味着它不仅会生成密文,还会生成用于完整性保护的认证标签。后者的处理工作会导致大约 28% 的性能下降。然而,如果你必须为 AES-CTR 添加一个 MAC 生成算法(例如 HMAC-SHA-256),这可能会比 28% 更加耗费性能。
如果安全性比吞吐量更重要,使用 AES-256-GCM 仍然能获得 12.1MBps 的合理性能。然而,如果性能是你的关键需求,你可以通过 AES-128-CTR 达到最低的机密性保护要求,速度几乎是它的两倍——即 22.9MBps。
此时,您可能会记得 ChaCha 流密码的软实现通常优于 AES 的实现。而且由于您可能是那种追求产品完美的人,您应该检查手头的软件是否支持该算法,如清单 4-4 所示。
# openssl list -cipher-algorithms | grep -i chacha
ChaCha20
ChaCha20-Poly1305
ChaCha20 @ default
ChaCha20-Poly1305 @ default
清单 4-4:给定 OpenSSL 工具中 ChaCha 密码算法的可用性
对 ChaCha20 和 ChaCha20-Poly1305 进行速度测试,得到了如清单 4-5 所示的结果。
# openssl speed -elapsed -evp ChaCha20
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
ChaCha20 21168.64k 36814.83k ... 57442.30k 57507.84k
# openssl speed -elapsed -evp ChaCha20-Poly1305
...
type 16 bytes 64 bytes ... 8192 bytes 16384 bytes
ChaCha20-Poly1305 16054.38k 28477.87k ... 47390.72k 47300.61k
清单 4-5:ChaCha20 和 ChaCha20-Poly1305 密码算法的性能测试
如果您的团队愿意从 AES 切换到 ChaCha20,它可以在数据速率达到 47.3MBps 或更高的情况下,获得 256 位安全性和认证加密。因此,ChaCha20 可能值得重新考虑。
SHA-256 哈希的软硬件实现对比
假设您的设备生成的日志文件按 100MB 分割,且您希望在文件离开设备之前对其进行签名,以确保完整性和真实性保护。由于输入数据量相对较大,签名操作的性能主要取决于哈希步骤,而不是最后的非对称签名。因此,比较 STM32MP157F 设备上HASH1硬件模块的 SHA-256 软件实现与加速器的性能,可能是值得的。
清单 4-6 展示了硬件支持的所有哈希函数,并通过其相应的驱动程序提供。
# cat /proc/crypto | grep stm32-sha*
driver : stm32-sha256
driver : stm32-sha224
driver : stm32-sha1
清单 4-6:STM32MP157F 硬件支持的 SHA 算法
可以通过加载cryptodev内核模块并将-engine devcrypto添加到速度测试参数中,使硬件加速可供 OpenSSL 命令行工具使用。清单 4-7 展示了 OpenSSL 对 SHA-256 的软硬件实现基本对比。
# openssl speed -elapsed -evp sha-256
...
type 16 bytes ... 1024 bytes 8192 bytes 16384 bytes
sha-256 2868.42k ... 24365.74k 27598.85k 27841.88k
# modprobe cryptodev
# openssl speed -elapsed -evp sha-256 -engine devcrypto
Engine "devcrypto" set.
...
type 16 bytes ... 1024 bytes 8192 bytes 16384 bytes
sha-256 127.87k ... 5835.78k 29996.37k 42592.94k
清单 4-7:SHA-256 性能在软件和硬件中的对比
对于像 16 字节这样的较小输入数据,软件解决方案的性能是硬件的 22 倍。这是因为测试数据必须从用户空间移动到内核空间,再到硬件并返回,这会带来显著的开销。然而,随着数据量的增加,这种效应变得越来越不相关。似乎对于 8KB 及更大的输入数据,STM32MP157F 硬件在性能上有优势。
注意
即使大多数硬件供应商将其加密模块称为“加速器”,也不能保证它们能加速任何操作。在您的特定情况下,使用硬件可能甚至会拖慢加密操作。在做出选择之前,请确保进行性能测试。
OpenSSL 命令行工具的标准数据大小上限为 16KB,但对于我们的特定用例,如果在哈希 100MB 文件时能够实现更高的吞吐量,将会是非常有趣的。清单 4-8 中的命令为调用添加了-bytes 104857600和-seconds 60选项,告诉 OpenSSL 使用 100MB 的输入块并大约进行一分钟的哈希操作。
# openssl speed -elapsed -evp sha-256 -bytes 104857600 -seconds 60
...
type 104857600 bytes
sha-256 27566.90k
# openssl speed -elapsed -evp sha-256 -engine devcrypto -bytes 104857600
-seconds 60
Engine "devcrypto" set.
...
type 104857600 bytes
sha-256 68021.40k
清单 4-8:使用 SHA-256 哈希 100MB 数据的性能分析
结果数据显示,软件实现并未从大输入数据中获益,但硬件实现能够将吞吐量提高到约 68.0MB 每秒。
健康检查是减少错误、误解甚至漏洞的小步骤。在使用硬件加密时,我强烈推荐进行这些检查。
首先,我想知道硬件是否真正被使用,还是软件回退操作让我误入歧途。其次,如果芯片制造商提供的性能数据与我的实验数据相匹配,我对所选解决方案的信心将会增加。清单 4-9 展示了回答这些问题的实用方法。
# time openssl speed -elapsed -evp sha-256 -bytes 104857600 -seconds 60
You have chosen to measure elapsed time instead of user CPU time.
Doing sha-256 for 60s on 104857600 size blocks: 16 sha-256's in 60.87s
...
type 104857600 bytes
sha-256 27562.37k
real 1m 1.79s
user 1m 0.94s
sys 0m 0.75s
# time openssl speed -elapsed -evp sha-256 -engine devcrypto -bytes 104857600
-seconds 60
Engine "devcrypto" set.
You have chosen to measure elapsed time instead of user CPU time.
Doing sha-256 for 60s on 104857600 size blocks: 39 sha-256's in 61.19s
...
type 104857600 bytes
sha-256 66831.94k
real 1m 2.26s
user 0m 0.18s
sys 0m 2.08s
清单 4-9:SHA-256 硬件哈希的健康检查
前缀time命令分析随后调用的进程的执行时间,并按三类划分:经过的挂钟时间(real),在用户空间花费的处理时间(user),以及用于特定进程的内核空间操作的时间(sys)。
软件-only 分析耗时 1 分钟 1.79 秒,其中 1 分钟 0.94 秒花费在用户空间,仅 0.75 秒用于内核操作。剩余的 61.79 – 60.94 – 0.75 = 0.10 秒可能是由于操作系统调度其他进程或执行独立的操作系统任务所导致的。
查看硬件加速的运行情况,情况完全不同。速度测试持续了 1 分钟 2.26 秒,但仅有 0.18 秒分配给用户空间计算,2.08 秒花费在内核空间。尽管这些数字较低,但 SHA-256 处理了 39 个 100MB 的输入数据块。
第一个结论是 62.26 – 0.18 – 2.08 = 60.00 秒没有出现在结果中。除了前面提到的调度和操作系统相关任务外,这部分时间还包括等待硬件组件处理并返回数据时的延迟。在 STM32MP157F 设备的参考手册 RM0436中,ST 解释了 SHA-256 的一个 512 位中间块的处理时间需要 66 个周期。因此,针对这一特定案例的纯硬件操作所需的时间估算如下:

这个数字至少在数量级上是正确的,但它仍然表明 60.00 – 15.82 = 44.18 秒在操作系统任务、驱动程序和其他硬件过程(如总线传输)中“丢失”。如果性能是你的首要目标,分析和优化驱动程序实现可能是下一步。
非对称加密软件性能比较
非对称加密操作在计算上是非常昂贵的。然而,了解它们的成本有多高以及可用选项在性能上的差异非常重要。
列表 4-10 中的第一次分析结果显示了 RSA 实现中密钥长度的显著影响。
# openssl speed -elapsed rsa1024 rsa2048 rsa4096
...
sign verify sign/s verify/s
rsa 1024 bits 0.004880s 0.000204s 204.9 4897.8
rsa 2048 bits 0.028736s 0.000672s 34.8 1487.6
rsa 4096 bits 0.178246s 0.002454s 5.6 407.5
列表 4-10:带有 1,024、2,048 和 4,096 位密钥的 RSA 性能分析
虽然过时的 RSA-1024 在我的 STM32MP157F 设备上每秒完成大约 205 个签名,但采用 2,048 位密钥的最新版本仅能完成每秒 35 个签名。通过加倍密钥长度,我们必须接受性能下降大约 6 倍的结果。转向 4,096 位变种后,每秒只剩下五到六个签名,这意味着签名操作需要超过 178 毫秒,尽管该设备的运行频率已经达到 800 MHz。
另一方面,显然签名验证(等同于加密操作)在所有密钥长度下表现出明显更高的性能,因为它利用了短 RSA 公钥指数。
在 ECDSA 方面,OpenSSL 工具提供了一套大规模的曲线。列表 4-11 给出了使用中一些最流行的 NIST 曲线的性能概述。
# openssl speed -elapsed ecdsap224 ecdsap256 ecdsap384 ecdsap521
...
sign verify sign/s verify/s
224 bits ecdsa (nistp224) 0.0090s 0.0074s 110.8 134.6
256 bits ecdsa (nistp256) 0.0010s 0.0028s 982.3 355.5
384 bits ecdsa (nistp384) 0.0322s 0.0243s 31.1 41.1
521 bits ecdsa (nistp521) 0.0773s 0.0567s 12.9 17.6
列表 4-11:选定 NIST 曲线的签名和验证性能
我们可以立即看到,NIST 曲线 P-256 比其他曲线快得多,因为它的结构和实现经过高度优化。此外,性能成本因素(例如,对于 RSA)的粗略估计是很难提供的。通常,测量特定平台上的特定实现是最可行的方式。
关于 ECDSA 和 RSA 的比较,NIST 的 P-224 和 RSA-2048 具有相似的安全级别,但在签名性能上有大约 3 倍的差异,ECDSA 更有优势。然而,在验证速度方面,RSA-2048 比 P-224 曲线快了超过 11 倍。
最后,替代椭圆曲线通常由于对 NIST 选择的信任问题而被考虑,但性能也可以是一个积极的方面,如 列表 4-12 中所示。
# openssl speed -elapsed ed25519 ed448 ecdsabrp256t1 ecdsabrp512t1
...
sign verify sign/s verify/s
256 bits ecdsa (brainpoolP256t1) 0.0099s 0.0084s 101.5 119.1
512 bits ecdsa (brainpoolP512t1) 0.0420s 0.0317s 23.8 31.6
sign verify sign/s verify/s
253 bits EdDSA (Ed25519) 0.0008s 0.0021s 1301.0 478.7
456 bits EdDSA (Ed448) 0.0059s 0.0120s 169.4 83.1
列表 4-12:具有有趣性能的替代椭圆曲线
Brainpool 曲线 P512t1 在与 NIST P-521 提供相似安全级别的情况下表现更好。此外,Bernstein 的 Ed25519(Curve25519)展现出卓越的性能,甚至超过了 NIST P-256。
假设你可以自由选择适合你应用的非对称签名算法,从性能角度来看,Ed25519 是一个非常有趣的候选算法。如果需要与 RSA 兼容,RSA-2048 目前可能是一个稳妥的选择,但请确保你的设备也能支持 RSA-4096,以便为未来的更新做好准备。
总结
性能不是一切。然而,在加密算法的实现过程中,它是一个你绝对不应该忽视的属性。除了安全性本身,性能是加密技术的基本质量特性之一。
一些嵌入式系统在处理能力、内存大小或功耗方面有很大的限制,这使得优化的实现变得不可或缺。否则,加密可能在权衡讨论中处于劣势。其他设备则专门用于网络或数据处理,要求高速的加密实现。有时候,性能要求可以通过使用高效的软件库来满足,但在其他场景下,则需要专门的数字硬件设计,如 FPGA(这本身是一个工程领域)或专为此任务设计的硬件协处理器。
请注意,即使你选择的加密算法从数学角度来看是安全的,并且展现出良好的性能特征,它们也不一定能抵御像侧信道分析和故障注入等实现攻击。Jasper van Woudenberg 和 Colin O'Flynn 的《硬件破解手册》(No Starch Press,2021 年)充满了关于如何破解和保护加密实现的实用示例和见解。Stefan Mangard、Elisabeth Oswald 和 Thomas Popp 的《电力分析攻击:揭示智能卡的秘密》(Springer,2007 年)也是深入研究这一领域的经典之作。
同时,请记住,这些先进的保护措施往往伴随着加密性能的下降。在开发生命周期的早期,务必尽早确定你的产品是否需要特别加固的加密实现。
第五章:机密数据存储和安全内存

嵌入式系统存储和处理各种数据。其中一些数据是无关紧要的,因为它们在许多平台上相同,或者可以很容易地猜测或推导出来。然而,某些数据通常被称为敏感、机密或保密数据,需要谨慎处理。
这些数据所携带的信息具有价值,其披露可能会导致负面后果。一个例子是知识产权,如嵌入到软件算法、专有协议和应用内容中的内容。此外,加密材料,如密钥和密码,天生就包含了关键信息。设备的生命周期数据,如传感器数据、日志条目和发送或接收的消息,也可能属于这一类。
本章首先讨论应以保密方式存储的数据以及在嵌入式系统中如何保持机密的困境。接下来,我将描述从操作系统级别到硬件再到混淆软件的存储机密数据的选项及其相应的优缺点。本章中的案例研究将通过使用加密文件容器在运行 Linux 的嵌入式系统上进行演示。
机密数据
识别机密数据并意识到相关威胁始终是保护数据的第一步。以下示例旨在帮助建立必要的思维方式。
想象一下,您的研究部门发明了一种超高效的发动机控制算法,能够使汽车的燃油消耗远低于竞争产品。虽然您可能已经申请了专利,但这可能不足以保护您的设备免受盗版和模仿者的侵害。发明的价值只有在您将其实施到控制设备中时才会实现。在此,它很可能作为一个已编译的软件可执行文件,或者作为内核模块的一部分存储。两者都存储在设备的文件系统中,如果攻击者能够读取文件系统,他们就能逆向工程您的算法,并直接从您的研究投资中获益。这种情况显然是您想避免的,这意味着您必须保护控制算法实现的机密性。
一些家庭和消费设备包含大量的媒体内容,如图片和视频,它们的制作成本非常高。将它们存储在设备文件系统中显而易见的位置通常不是一个好主意。攻击者可能会复制这些内容,将它们上传到他们喜欢的在线平台,并为了自己的目的重用它们,这不仅可能造成经济损失,还可能损害您的产品和公司的声誉。
乍一看,保护加密材料似乎是显而易见的。然而,许多设备将与其证书基础身份验证对应的私钥存储在标准文件系统中。读取和复制这些密钥为冒充攻击打开了大门。此外,特别是在应用层,控制用户身份验证的密码有时会以明文形式存储在配置文件或软件二进制文件中。当然,提取密码会让攻击者成功地进行身份验证,因为凭证并没有以保护机密性的方式存储。
在某些情况下,真正有趣的数据是在设备的生命周期中生成的。例如,考虑产品通过指纹识别器收集的生物特征数据或通过位置数据创建用户移动档案的隐私影响。这些信息通常有很高的保密要求,甚至可能受到如欧洲通用数据保护条例(GDPR)等法律的约束。如果保护措施失败,可能会导致高达 2000 万欧元或公司全球总营业额的 4%的罚款。
设备中存储的数据历史甚至可能揭示更多内容。例如,在工业生产环境中,数据历史可能允许攻击者重建机器使用率和产出数据,这些可能是竞争对手所看重的有价值信息。同样,保密保护将是设备的重要特性。
几乎每个现代设备都包含值得保护的机密数据。在进行威胁和风险分析时,请务必牢记这一点。
嵌入式系统中的保密困境
嵌入式系统通常包含一种有毒的属性组合。首先,它们必须能够在没有用户交互的情况下启动并运行,这意味着所有与正确操作相关的信息必须存储在设备内部,包括所有的机密数据。其次,攻击者通常可以获得设备并对其进行彻底分析。这种分析不仅包括基于网络的调查,还包括对产品内部通信线路的物理窃听,以及提取如固件镜像和文件系统分区等非易失性存储内容进行逆向工程。 从理论上讲,对于可以在所有层面进行分析的设备,其机密性保护是无法得到保证的。
然而,从更实际的角度来看,问题始终是:“攻击者需要花费多少努力才能全面了解设备及其机密数据?”
让我们考虑一下我之前提到的知识产权示例。假设你负责保守一个高度复杂算法的软件实现的秘密,尽管它必须存储并在你的设备上使用。有些人可能会立即认为,从闪存读取固件、识别可执行文件并进行算法的逆向工程非常复杂,因此他们认为不需要任何保护措施。
这个假设对于像脚本小子这样的攻击者可能是成立的。然而,考虑到那些寻求经济利益的罪犯,对他们来说,转储闪存和挂载文件系统似乎并不算太多工作。此外,像 Radare2 和 Ghidra 这样的免费开源工具使得任何有兴趣的人都可以进行至少基本的软件逆向工程。如果机密性在你的产品中具有更高的优先级,那么这些攻击者绝对应该列入考虑范围。
当然,有些情况攻击起来要简单得多。对于那些将固件存储在可移动介质(如许多现成笔记本电脑可以读取的 microSD 卡)上的设备,甚至不需要进行闪存转储。如果机密信息(如 RSA 私钥)以像隐私增强邮件(PEM)这样的可直接重用格式存储,那么“攻击”可以在短时间内完成,甚至由一个脚本小子来做。而且,并不是每个可执行文件都需要逆向工程。例如,代码提取攻击只需将二进制文件原封不动地复制到另一台设备上并运行。如果它实现了预期功能,就不需要访问其专有的细节。
安全文件系统方法
到这个时候,你可能会想:“是否存在加密文件系统?那将解决我们所有的问题。”确实存在,但它并不能完全解决问题。有三种常见的选项可用,它们为文件提供机密性保护:加密堆叠文件系统、原生文件系统加密和加密块设备。
加密堆叠文件系统
堆叠文件系统是在现有文件系统之上增加的一个额外文件系统结构,这意味着像 ext3 这样的标准文件系统不会被修改,加密发生在其上层。在这种情况下,文件的内容和名称会被加密——例如,对于特定目录中的所有文件。然而,文件的数量及其元数据是可以读取的。
堆叠文件系统也会带来一定的性能开销,并且文件名可能会受到额外的限制。多年来,使用用户空间文件系统(FUSE)框架的 EncFS 一直是这一方法的流行且易于使用的示例,但似乎开发已经停滞。一个有潜力的继任者可能是 gocryptfs。
一个在内核空间运行的竞争者是 eCryptfs。虽然 eCryptfs 可能配置起来稍微复杂一些,但在某些情况下它表现出更好的性能。然而,它的开发也停滞不前。基于文件的加密(FBE)与堆叠文件系统似乎已经不再流行。
原生文件系统加密
目前,像 ext4、F2FS 和 UBIFS 这样的文件系统直接支持文件的加密存储。与文件系统堆叠相比,这允许更高效的集成和操作。
这种原生 FBE 方法的流行用户包括 Android 和 Chrome OS。其底层功能已在 Linux 内核中实现,并使用内核的加密 API 进行加密。用于配置和管理加密目录的用户空间工具叫做 fscrypt。
加密块设备
基于文件的方法总是将文件夹结构和元数据保留为未加密状态。一种常见的避免信息泄露的方式是全盘加密(FDE)。在 Linux 中,这意味着加密层位于文件系统下方,位于块设备级别。无论该块设备是整个磁盘、分区还是文件容器,它都会作为一个整体进行加密,其内容与随机信息无法区分。
对于 Linux 来说,这类加密技术最常见的代表是 dm-crypt,它基于设备映射器基础设施,并使用内核的加密 API。cryptsetup 用户空间工具可以创建加密卷,并且支持流行的 Linux 统一密钥设置(LUKS)容器格式,该格式为加密卷提供了多种密钥管理功能。
TrueCrypt 的继任者 VeraCrypt 是另一个流行的工具,例如,它还支持将两种密码算法串联起来,以增强解密的抗性。
建议
加密数据存储显然不缺少选择。FDE 或 FBE 是否适合你的需求,主要取决于你的设备及其相应的安全要求。
如果从你的角度来看,文件的位置、数量和大小已经向攻击者透露了敏感信息,那么 FDE 可能是带来最高安全性的解决方案。然而,FBE 提供了更多的灵活性——例如,在同一文件系统内托管异构文件集,以便某些数据始终可以被所有进程访问和读取,但机密数据则按目录选择性解密。它还可以具有应用特定的密钥,从而实现更细粒度的机密性保护。
密码短语
无论你选择哪种实现方式,一个大问题始终存在:所有工具都需要一个密码短语,以直接解锁受保护的目录和卷,或解锁用于解密数据的密钥文件。即使是“密码短语”一词也表明这一概念是让用户像我们在 PC 启动时输入凭证一样使用的。
然而,大多数嵌入式系统并没有活跃的用户在其面前,能够在启动时输入密码短语来解锁硬盘加密。接下来通常会问:“在哪里隐藏解锁加密的密钥?”
硬件中的安全内存
我时常听到一些人(和公司)说,硬件实现的安全内存是解决嵌入式系统凭证问题的方案。然而,这只是部分正确。
硬件实现的安全内存的一个优势在于,它在经典的非易失性内存(存储固件的位置)和专用的安全模块之间实现了强有力的物理分隔,而该安全模块只能通过特定的接口访问。此外,它还支持篡改检测或抗篡改功能,并且存储的位数据可能深埋在芯片内部,这使得硬件攻击变得困难,并且需要复杂的设备。
然而,这种方法也有其缺点。最明显的是,这种受保护的内存通常只能存储少量数据,通常是加密密钥。此外,这些密钥通常必须离开安全模块才能发挥作用,比如解锁 LUKS 容器。在这种情况下,攻击者可能会在通信线路上传输过程中捕获这些密钥。
在过去,当外部安全模块将密钥传递给主 CPU 时,攻击者通过窃听物理 PCB 路径来利用这一问题。其他攻击则主动与安全内存进行通信,例如要求它为攻击者加密、解密或签名任意数据。因此,仅仅拥有一个安全内存并不会显著提升设备的安全性;它还必须安全集成。
和往常一样,是否使用安全内存是否合理取决于具体的应用场景和实现细节。通常,它至少为攻击者添加了另一层需要了解的知识——即观察设备运行时的安全内存通信,而不仅仅是从固件或文件系统中提取机密信息。让我们来考虑一下与安全内存相关的选项。
外部安全内存
将“传统”方式添加安全内存设备到设计中的方法是将一个专用微芯片集成到设备的 PCB 上,并将其连接到主微处理器。这个领域中最突出的代表是受信平台模块(TPM)。
你可以从多个制造商那里购买离散的 TPM 设备,比如英飞凌、NXP 和 ST。它们在 PC 世界中已经得到了广泛应用,因为微软的 Windows 系统要求它们作为硬件必备组件。一个基本的用例是,用户可以解锁一个 TPM 密钥,然后该密钥用来解锁由 BitLocker 保护的卷。当然,这只是该过程的简化视图,问题也正是出在这里。
TPM 远不止是安全内存。它包括授权层、密钥层级,并且当前 TPM 版本的规范超过 1000 页。尽管 Linux 下有可用工具来使用 TPM,如tpm2-tools和ibm-tss,但使用 TPM 对普通产品和开发团队来说似乎太复杂。
幸运的是,安全微控制器制造商提供了针对汽车、工业和物联网应用的有趣替代方案。类似 TPM 的产品,如英飞凌的 OPTIGA Trust X、NXP 的 Edge-Lock SE050 系列和微芯的 ATECC608B,配备了广泛的加密算法,从对称密码(如 AES)、哈希函数和 HMAC,到非对称加密(如 RSA 和 ECDSA)。当然,它们也提供用于加密密钥的安全内存。它们的承诺是,这些设备需要的集成工作量远低于物联网产品。
总结来说,单纯将 TPM 用作安全内存就像是用大锤打蝴蝶。对于嵌入式系统而言,更精简的替代方案可能更为合适。然而,在决定是否使用专用硬件安全模块时,你还应考虑到安全设备身份和安全通信等主题,正如在第六章和第七章中所讨论的那样。无论如何,确保你理解可能通过接口与物理攻击者可接触的渠道传输秘密密钥的含义。
注意
TPM 2.0 规范提供了一种加密主机 CPU 与 TPM 之间通信的方式,这为攻击者和工程师都增加了复杂性。
内部安全内存
与需要设计并焊接到 PCB 上的离散硬件组件相比,安全模块和安全存储设施已经深度集成到设备的主 CPU 中。
被称为集成 TPM或固件 TPM的这些 TPM 替代方案,由 AMD 和 Intel 等芯片厂商提供。除了降低 PCB 集成成本外,这一概念的显著优势是片上通信比 PCB 上运行的信号更难以捕获。当然,TPM 的复杂性问题依然存在。
另一方面,现代 SoC 通常提供一次性可编程(OTP)内存,可用于存储至少一个主密钥。一个常见的例子是 NXP i.MX 系列的级联加密概念。在这里,主密钥(OTPMK)在生产过程中被编程到设备中。随后,使用此密钥来加密数据加密密钥(DEK),以获取所谓的 DEK 二进制大对象(BLOB)。然后,开发人员可以使用 DEK 来加密应用程序数据。
图 5-1 展示了从主密钥 OTP 读取到应用数据解密的过程。

图 5-1:NXP i.MX 系列的解密级联
这个过程为设备增加了安全性,因为密钥和机密数据不会以明文形式存储,同时,必要的密钥直接加载到设备的加密加速和保障模块(CAAM),在这里进行解密,这意味着只有解密后的用户数据在运行时离开这个内部模块。
应用程序代码中的秘密
由于成本或旧硬件无法再升级,额外的硬件或安全增强型主 CPU 并非总是可行的。将密钥明文存储是不可避免的吗?不一定。
我们仍然可以将密钥从文件系统的结构化字段移到更为拥挤的可执行文件形式中。同样,这种方法要求潜在攻击者具备更高的能力——即二进制逆向工程。即使对手得知某个特定的可执行文件负责解锁加密的卷,他们也无法立即提取解锁密钥。
注意
是的,安全通过模糊化实际上出现在一本安全工程书中。在现实世界的设备工程中,你确实有时需要抓住一根稻草。
描述这种对策的术语是混淆。其唯一目的是使软件逆向工程更加困难,可以通过多种方式实现:
-
用复杂但等效的操作替代常见操作
-
通过重新排列命令顺序来使控制流更加复杂,但不破坏功能
-
插入冗余、不必要的代码和数据
-
将部分代码或数据以加密或编码形式存储,并仅在运行时解密或解码
-
向数据结构和控制流中添加随机性
-
集成防调试措施,以阻碍分析
-
将密钥拆分成多个部分,存储在不同的位置,并仅在运行时组合它们
这个列表并不全面,每个要点都可以通过多种方式实施,并具有多种创造性。如果你自己做不到,也可以使用提供自动代码混淆功能的工具。不过,使用这些工具时必须小心,因为每种混淆工具可能也有对应的对立工具,即反混淆工具,它可以还原添加的复杂性,使攻击者能够更高效地分析代码。
2002 年,名为白盒加密的概念出现在学术讨论中,作为解决在软件中隐藏秘密问题的一种方法。这种方法旨在在软件中执行加密操作,而不揭示底层的秘密。这意味着使用的加密密钥会扩散到加密算法的操作中。
白盒加密的最初概念将硬编码密钥的加密算法转变为基于密钥的表查找,并将随机值注入内部计算中,这些值在某些时候会掩盖数据,并在之后揭示出来。然而,毫不奇怪,这导致了二进制文件大小和性能上的显著开销,并且这些实现也被密码分析攻破了。虽然学术界尚未找到完美的解决方案,但许多软件公司在其产品中使用了某种形式的白盒加密——例如,在允许用户下载二进制文件并进行分析的移动应用中。
无论你使用自定义的混淆技术、商业工具,还是学术领域的白盒加密方法,在将秘密信息隐藏在软件二进制文件中的时候,你始终应该牢记一种攻击者的方式:代码提升。在这种情况下,二进制文件“按原样”使用——例如,解锁一个 LUKS 容器。如果攻击者只是需要将可执行文件作为加密保险库的密钥,那么密钥本身对攻击者来说并不重要。这可能发生在原地,或者感兴趣的文件可能会被提取并在攻击者控制的环境中执行。
安全的密码存储
基于密码的身份验证是验证合法用户登录的常见方法,但它也用于解锁加密的卷和容器。自然,密码是机密数据。将它们以明文存储在文件或可执行文件中以便在验证过程中进行比对,会让攻击者变得很容易。一旦密码被提取,它们就可以被成功地用于访问机密数据。然而,如同我们讨论的其他数据保护措施一样,传统的加密通常并不适用于这里。密码的特定验证过程允许实现一种不同的安全存储方式:密码哈希。
哈希函数的单向特性对于防止攻击者从哈希值中恢复出密码非常有用,即使哈希值已从设备中提取。验证过程仍然有效,因为给定的密码可以高效地进行哈希计算,并与存储的值进行比较。然而,在这种简单的情况下,攻击者可能会预计算彩虹表,这是一个包含数百万个密码哈希值的大型集合,甚至可能包括所有可能的 10 个字符的密码。为了防止此类攻击,会在密码哈希过程中添加一个随机的盐,使每个哈希值都独一无二,并使所有彩虹表失效。
Linux 目前仍默认使用基于 SHA-512 哈希的函数来生成加盐的用户密码哈希。此外,基于密码的密钥派生函数 2(PBKDF2)在 20 多年前已经在 RFC 2898 中标准化。它基于 HMAC,使用像 SHA-1、SHA-256 或 SHA-512 这样的哈希函数。两种方法无疑比明文密码存储要好,但现代攻击者使用图形处理单元(GPU)、现场可编程门阵列(FPGA)和专用的应用特定集成电路(ASIC)通过暴力破解密码哈希。
因此,为了使产品具备未来保障,建议使用该领域的现代代表。典型的例子有 scrypt,它在 2016 年被标准化为 RFC 7914;它的继任者 yescrypt;以及在 2015 年赢得密码哈希竞赛的 Argon2。这些算法的共同目标是通过可参数化的密码哈希算法增强破解抗性,这些算法考虑了计算时间、所需内存和并行 CPU 线程数量等维度,旨在阻止那些即使拥有多种硬件资源和计算能力的攻击者。
注意
即使保密性是密码的主要保护目标,但如果有人能够更改密码,后果将非常严重。用已知密码的哈希替换 root 用户的密码哈希是一个相当常见的攻击,它为对手打开了所有大门。你应该认真考虑对密码存储的完整性保护,如第八章中讨论的那样。
案例研究:Linux 上的加密文件容器
在这个案例研究中,假设你的一个遗留设备没有实现任何安全存储来保护机密数据。在参加一个国际展会时,你的工程团队成员注意到一个目前未知的竞争对手使用了你们的一款专有 GUI 应用程序和你们精心制作的相关媒体内容,满足了目标受众的需求。
回到家后,你与团队和管理层讨论了这个问题。经过初步调查,显然,窃取数据所需的只是打开设备并从闪存中读取内容,而你的竞争对手显然已经成功做到了这一点。为了限制未来的损失,你决定通过使用 LUKS 容器至少建立一个基本的保护措施。你当前的闪存设备容量为 256MB。基本的 Linux 系统及所有相关工具大约占用 136MB。你的专有可执行文件及其数据和配置文件大约占 19MB,而高质量的视频内容占用 73MB。
你想保护后两类数据。由于平台上的内存需求相当紧张,而且你希望将代码与媒体分开,你计划创建一个 25MB 的容器用于可执行文件和包含知识产权的文件,以及一个 90MB 的容器用于视频文件。
加密基准测试
作为第一步,你将 cryptsetup 添加到设备的根文件系统中,以便能够使用 LUKS 容器。你还在 Linux 内核配置中启用了 CONFIG_CRYPTO_SERPENT 和 CONFIG_CRYPTO_TWOFISH,因为你听说 cryptsetup 支持它们,并且想知道它们是否能在你的 STM32MP157F SoC 上超越 AES。清单 5-1 显示了 cryptsetup 自带性能基准测试工具的结果。
# cryptsetup benchmark
# Tests are approximate using memory only (no storage IO).
PBKDF2-sha1 59148 iterations per second for 256-bit key
PBKDF2-sha256 92695 iterations per second for 256-bit key
PBKDF2-sha512 58618 iterations per second for 256-bit key
PBKDF2-ripemd160 49201 iterations per second for 256-bit key
PBKDF2-whirlpool 14021 iterations per second for 256-bit key
argon2i 4 iterations, 65536 memory, 4 parallel threads (CPUs)
for 256-bit key (requested 2000 ms time)
argon2id 4 iterations, 65536 memory, 4 parallel threads (CPUs)
for 256-bit key (requested 2000 ms time)
# Algorithm | Key | Encryption | Decryption
aes-cbc 128b 36.9 MiB/s 36.8 MiB/s
serpent-cbc 128b 9.0 MiB/s 10.4 MiB/s
twofish-cbc 128b 11.1 MiB/s 12.0 MiB/s
aes-cbc 256b 36.9 MiB/s 37.0 MiB/s
serpent-cbc 256b 9.1 MiB/s 10.5 MiB/s
twofish-cbc 256b 11.3 MiB/s 12.0 MiB/s aes-xts 256b 18.4 MiB/s 17.0 MiB/s
serpent-xts 256b 9.0 MiB/s 10.4 MiB/s
twofish-xts 256b 11.5 MiB/s 12.0 MiB/s
aes-xts 512b 15.3 MiB/s 13.1 MiB/s
serpent-xts 512b 9.6 MiB/s 10.5 MiB/s
twofish-xts 512b 12.0 MiB/s 12.0 MiB/s
清单 5-1:由 cryptsetup 提供的基准测试结果
最初,这个输出显示了多个密码存储功能的分析,帮助你了解哪些特定功能的参数可能是你设备的合理选择。你希望将像迭代次数这样的参数设置为可以在你的设备上提供可接受性能的值(例如,解锁时间在两秒内),同时让攻击者的破解工作量最大化。在这种情况下,由于没有特定的需求,你选择保持 LUKS2 卷的默认设置:Argon2。
加密/解密基准测试显示,无论是使用 AES 的 CBC 模式还是 XTS(基于 XEX 的调整密码本模式带有密文盗取),AES 都是最快的算法。AES-CBC 性能似乎还得到了 STM32MP157F 加密硬件的加速。另一方面,XTS 是 cryptsetup 和硬盘加密的一般推荐默认操作模式。由于你非常重视性能,并且不希望容器加密影响任何设备功能,你选择了 AES-CBC,并且由于 128 位和 256 位密钥几乎没有差异,你选择了 256 位密钥。
注意
你可能会好奇 XTS 如何使用 512 位密钥与 AES 配合使用。简而言之,它并没有!它需要两个密钥(在这种情况下是两个 256 位密钥,总共加起来是 512 位),但安全级别仍然是 256 位。
容器创建
在确定加密参数后,你可以开始为你的两个容器创建随机文件和相应的密钥文件。用于可执行文件的容器占用 25MB,媒体容器则基于一个 90MB 的文件,如 清单 5-2 所示。两个密钥都初始化为 32 字节,即 256 位。
# dd if=/dev/urandom of=executables.enc bs=1M count=25
# dd if=/dev/urandom of=media.enc bs=1M count=90
# dd if=/dev/urandom of=executables.key bs=32 count=1
# dd if=/dev/urandom of=media.key bs=32 count=1
清单 5-2:容器和密钥文件的随机初始化
在下一步,如 清单 5-3 所示,你使用 cryptsetup 通过 luksFormat 命令提供 LUKS 容器的基本结构。
# cryptsetup -q -v --type luks2 --cipher=aes-cbc-essiv:sha256
luksFormat executables.enc executables.key
# cryptsetup -q -v --type luks2 --cipher=aes-cbc-essiv:sha256
luksFormat media.enc media.key
清单 5-3:LUKS2 容器的创建
由于你希望使用 Argon2 进行密码验证,并且想要享受 LUKS2 中第二个头部副本带来的鲁棒性,你选择使用 --type luks2 来选择第二版本的头部格式。之前选择的基于 AES 的 CBC 模式并带有加密盐扇区初始化向量(ESSIV)的配置,通过 --cipher=aes-cbc-essiv:sha256 进行配置。
此时,容器及其结构已经准备好,但你还不能在这些虚拟金库中存储任何东西。需要通过 luksOpen 解锁文件容器,以在其中创建 ext3 文件系统。注意,解锁 LUKS 容器后,如清单 5-4 所示,它会映射到设备 /dev/mapper/dm_exec_enc,然后挂载到新创建的目录 /mnt/exec_enc。同样的操作也可以用于加密的媒体容器,其设备为 /dev/mapper/dm_media_enc,挂载点为 /mnt/media_enc。
# cryptsetup -v --key-file=executables.key luksOpen executables.enc dm_exec_enc
# mke2fs -t ext3 /dev/mapper/dm_exec_enc
# mkdir /mnt/exec_enc
# mount /dev/mapper/dm_exec_enc /mnt/exec_enc
清单 5-4:解锁容器以创建文件系统并挂载
看起来你已经快完成了。让我们使用 luksDump,如清单 5-5 所示,来检查是否设置了正确的属性。
# cryptsetup luksDump executables.enc
...
Keyslots:
0: luks2
Key: 256 bits
Priority: normal
Cipher: aes-cbc-essiv:sha256
Cipher key: 256 bits
PBKDF: argon2id
Time cost: 4
Memory: 65536
Threads: 2
...
清单 5-5:再次检查容器属性
输出完全符合我们的要求。
效率分析
在你开始填充文件系统之前,让我们最后检查一下这个构建的效率。fdisk 和 df 工具将帮助你了解 LUKS 头部损失了多少内存,以及 ext3 文件系统引入了多少开销。清单 5-6 中有详细信息。
# fdisk -l /dev/mapper/dm_exec_enc
Disk /dev/mapper/dm_exec_enc: 9 MiB, 9437184 bytes, 2304 sectors
...
# df -hT /dev/mapper/dm_exec_enc
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/dm_exec_enc ext3 4.5M 28K 4.0M 1% /mnt/exec_enc
# fdisk -l /dev/mapper/dm_media_enc
Disk /dev/mapper/dm_media_enc: 74 MiB, 77594624 bytes, 18944 sectors
...
# df -hT /dev/mapper/dm_media_enc
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/dm_media_enc ext3 66M 28K 62M 1% /mnt/media_enc
清单 5-6:关于内存使用效率的检查
不幸的是,这并没有按计划进行。25MB 的容器只剩下 4MB 的空间来存储数据,而另一个大小为 90MB 的容器则剩余 62MB。明显可以看到,容器大小与其在设备映射器基础设施中的相应设备之间的差异大约为 16MB。实际上,如果你仔细研究过 LUKS 文档,你会知道 LUKS1 和 LUKS2 的头部分别消耗了 2MB 和 16MB。虽然这对几百 GB 的硬盘来说不是问题,但对于内存受限的嵌入式系统来说,这可能会成为一个痛点。
你可以通过使用 LUKS1 头部而不启用 LUKS2 的改进来解决这个问题,这在实际操作中是可以接受的,或者甚至使用不在内存中存储任何元数据的 cryptsetup 的“纯模式”。后者可能是最有效的选择,但也带来了显著的管理限制。LUKS1 可能是一个合理的选择。
清单 5-6 还显示,ext3 文件系统由于需要分配表和日志数据,会减少可用内存。然而,对于静态存储可执行文件、配置文件和媒体内容,日志记录可能不是必需的,你也可以部署 ext2 文件系统以释放更多可用内存。
在将 luks2 更改为 luks1 和 ext3 更改为 ext2 后重复整个过程,情况有所不同,如清单 5-7 所示。
# fdisk -l /dev/mapper/dm_exec_enc
Disk /dev/mapper/dm_exec_enc: 23 MiB, 24117248 bytes, 47104 sectors
...
# df -hT /dev/mapper/dm_exec_enc
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/dm_exec_enc ext2 22M 14K 21M 1% /mnt/exec_enc
# fdisk -l /dev/mapper/dm_media_enc
Disk /dev/mapper/dm_media_enc: 88 MiB, 92274688 bytes, 180224 sectors
...
# df -hT /dev/mapper/dm_media_enc
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/dm_media_enc ext2 81M 14K 77M 1% /mnt/media_enc
清单 5-7:LUKS1 和 ext2 的内存使用效率
分别损失 4MB 和 13MB 的情况仍然发生,但剩余的空间足以满足本案例的需求。
注意事项
如果你在现实生活中遇到类似情况,但无法满足你的需求,你仍然可以考虑切换到本地文件系统加密(例如使用 ext4),或者使用压缩的只读文件系统,如 CramFS 和 SquashFS。
解决这些与安全无关的意外问题后,最后一个问题是如何安全地解锁已创建的容器。由于你的设备中没有安全元素,且主 CPU 也没有为此提供 OTP 内存,你可能决定将解锁过程隐藏在initramfs中的可执行文件里,该文件在启动时执行。需要明确的是,采用这种方法并不会为你赢得“安全设计”奖项,但它可能会提高成功攻击的门槛。
你可以考虑包括硬件基础的系统标识符,如第六章中所述,以衍生出设备唯一的解锁密钥,而不是仅依赖于容易在设备外部仿真的全球性软件解决方案。此外,你还可以实现秘密共享方法,例如将密钥文件内容拆分成多个部分,并在运行时动态组合它们以获得最终的密钥,这至少能够阻碍简单的静态分析攻击。无论如何,你必须意识到,获得 root 权限的攻击者能够绕过所有这些措施,并在运行时访问解密后的文件系统。对于下一代产品,你将从第一天起就将安全存储作为优先事项。
读取保护作为低成本解决方案
一些小型嵌入式系统成本低廉,缺乏安全内存,无法运行 Linux,且完全无法承担外部安全元素。这些设备可能基于运行实时操作系统(RTOS)的微控制器,甚至仅运行裸机软件。
然而,即使是这些情况,也可能有数据需要一定级别的机密性保护。由于这些设备通常将所有代码和数据存储在内部闪存中,限制对闪存的读取访问可能是一种简单但有效的保护敏感信息的措施。启用读取保护——例如,通过烧录相应的熔丝——将使设备能够执行内部存储的代码,但拒绝提取其非易失性存储器内容的请求。
即便如此,仍需注意,配备合适设备的物理攻击者能够绕过低成本微控制器的基本保护功能,从而访问你的机密数据。
总结
本章介绍了可能需要保密保护的几类数据,从知识产权到媒体内容,再到加密密钥。遗憾的是,对于在嵌入式系统中存储此类敏感数据,目前没有完美的解决方案。导致这一困境的一个原因是,与 PC 相比,嵌入式系统没有主动的用户,他们会将主密钥记在脑中并在需要时输入。
由于所有的秘密必须始终在同一设备上可用,我们能做的唯一事情就是将这些秘密隐藏在经过良好保护的地方,比如安全元件、内部 OTP 内存或加密软件中。所有这些解决方案的目标是增加攻击者需要突破的障碍,以实现成功的攻击,但每个方案都有其自身的优点和缺点。
许多方法将秘密的披露和使用推向设备的运行时阶段,这要求攻击者控制设备某些部分,甚至执行自定义代码。因此,保密保护的质量和成功与设备的运行时完整性有着密切关系,我们将在第八章中讨论这一点。
第六章:安全设备身份

长时间以来,嵌入式系统在幕后匿名运行,不关心远程访问、数字商业模式或与其他设备及云服务共享数据。然而,今天这些场景已经发生了根本性的变化。
突然间,维护人员现在通过远程登录设备,无法通过查看物理指示器来验证他们是否正在操作正确的设备。此外,按需付费的商业模式在工业场景中变得越来越流行,设备会自行生成账单。在这种情况下,能够证明使用数据的来源并将其映射到特定客户是至关重要的。此外,不同制造商生产的设备开始互相通信并交换数据。所有这些趋势有一个共同的强烈需求:每个设备需要一个唯一的身份标识,且每个设备必须能够证明这一点。
本章的第一部分调查哪些属性有助于设备的独特性,并能作为身份的基础,以及紧密相关的身份识别和认证过程。接下来,我们将从两个角度来探讨设备身份管理的实施:设备内存储的加密身份和制造商端的生命周期管理。本章最后通过两个案例研究探讨身份生成和供应。
每个设备都是独一无二的
消费品和工业组件的大规模生产可能给人一种印象,即所有从生产线下来的产品在固件的每个比特上都是相同的。然而,如果真是如此,你如何区分一个设备与另一个设备呢?当然,产品上长期以来都有带有序列号的贴纸,但如果贴纸掉了、故意被去除,甚至被更换成伪造的版本呢?
对于现代设备来说,唯一的身份应该是设备本身的一个重要组成部分,并且该组件应该能够主动向第三方设备、维修站以及原始制造商的云服务等证明其身份,仅举几个例子。
从理论上讲,即使每个设备拥有相同的 PCB、微处理器和 RAM,它们依然是独一无二的,因为所有这些单元都受到(即便是微小的)物料、时间行为、功耗等方面的个体差异的影响。学术界已经在利用这些微小物理特征的独特性来建立设备身份。相关的研究领域聚焦于物理不可克隆函数(PUFs),这些技术最近甚至已经出现在首批商业产品中。
以下章节将探讨当前设备中哪些因素使其在实际应用中具有独特性,并如何将这些唯一身份证明给其他方。
身份识别与标识符
显然,标识这个术语与身份这个词紧密相关。然而,花点时间思考一下它的确切含义。
如果我们要定义标识的过程,可以说它是“宣称某一身份”。例如,如果你在会议上遇到某人,你可能会说:“你好,我是乔!”你宣称你是乔。如果你的设备收集了一些使用数据——假设是在一个月内——然后连接到后台提供数据以供客户结算,它也可能会开始说:“你好,后台,我是 XY1337-0815!”它宣称自己是一个具有某种“名称”的设备。
唯一标识符
关于唯一性,告诉别人你是乔(Joe)显然是不够的。可能存在多个乔,甚至可能在同一个会议上。加上你的姓氏可能会缩小范围,但你的名字仍然不会是唯一的,至少在全球范围内是这样。如果你考虑到出生地点和出生日期,你会更接近于拥有一组唯一标识你的数据。这些属性被称为标识符。人类有更多的标识符:发色、眼睛颜色、身高、体重等等。
由于设备通常没有类似人类的名字,制造商必须采取另一种方法进行标识。长期以来,典型的标识符一直是厂商选择的值,如型号、序列号和生产日期。
随着互联网的出现,全球范围内的标识符需求变得明显。那时,UUID(即全球唯一标识符(GUID))的概念被提出。它在 RFC 4122 等标准中被规范化,旨在提供 128 位的唯一标识符,且无需中央注册过程。尽管标识符碰撞的概率不为零,但在实践中被认为非常接近零。UUID 的生成可以通过 Linux RNG 等方式完成,具体可见第三章。
从密码学的角度来看,由非对称加密算法(如 RSA 和 ECDSA)生成的公钥也可以完美地作为标识符。它们甚至可以与主体名称和其他属性结合,获得一个独特的设备证书进行标识,正如在网络认证标准 IEEE 802.1AR 中所规范的那样。
系统身份
一些设备由一个单一的核心组件组成,这个组件构成了整个设备及其身份,而其他产品架构则更加模块化,并允许在发生故障或硬件升级时进行部分更换。在后者的情况下,讨论哪些组件对设备的身份有贡献,哪些没有,是值得的。嵌入式系统的物理部分提供了多种标识符,如网络卡的媒体访问控制(MAC)地址、蓝牙芯片组和 Wi-Fi 控制器的 MAC 地址,还包括 CPU、闪存和可移动媒体的序列号和唯一标识符。
要求系统身份包含一组标识符,也意味着如果其中一个部分发生变化,系统身份必须重新生成或重新批准。这个要求对于制造商来说可能是一个优势——例如,强制用户购买相同品牌的备用零件——因为每次更换都需要制造商的确认。然而,系统身份和强制制造商批准也可能增加制造商方面的额外工作量。此外,如果每一个小的变化都需要与原始制造商进行反馈回路,它可能会显著限制运营商在日常业务中的自由度。
注意
有时候,设备的可靠性是最重要的目标,如果硬件出现故障,操作员必须立即替换它。在这种情况下,允许设备身份可转移是合理的——例如,使用可拆卸的存储卡。
身份验证和验证器
在日常语言中,身份识别和身份验证有时可以互换使用,但身份验证意味着的不仅仅是声称一个身份。
如果你的身份的有效性和正确性真的很重要——例如,如果你需要申请护照或注册投票——你告诉他们:“嗨,我是乔,”他们可能会回答:“嗨,乔,请出示你的身份证。”他们会让你证明你的身份——这与数字身份验证过程的模拟等价。
身份验证一词意味着你必须确认你在几秒钟前所声称的身份。为此,你需要拥有一个有效的验证器,它对应于给定的身份。对于人类来说,验证器可以是身份证、驾驶证等。对于所有这些身份证明,某个权威机构在某个时间点验证了人类的身份,并随后颁发了一个通常有效一定时间的相应验证器。在这个有效期内,权威机构和其他人可以使用提供的验证器来验证特定的身份。
对于设备,典型的验证器包括对称密钥或非对称私钥、(临时的)认证令牌或密码(在传统情况下)。这些验证器是在特定设备(例如,在生产过程中)为其创建并发行的,并且可以在以后通过密码学手段证明该设备的身份。
身份验证协议
根据验证器的类型,身份验证过程以不同的方式执行。一种常见的方法是挑战-响应身份验证协议。图 6-1 展示了挑战-响应握手的一种形式。

图 6-1:挑战-响应身份验证过程的典型步骤
挑战-响应认证过程从生成一个随机挑战 C ➊ 开始,挑战随后被传输到设备。设备利用其秘密认证信息处理这一不可预测的值,并产生一个响应 R,返回给验证方 ➋。在最后一步 ➌,R 将与预期值进行比较,以决定认证是否成功。
对于对称秘密信息,设备上的算法处理给定的挑战与设备特定的秘密信息时,可能是一个哈希函数或 HMAC 构造。然而,缺点是,秘密信息还必须在验证方的数据库中可用,以便计算正确的预期值,而不仅仅存储在设备内。
相比之下,非对称加密允许仅设备本身使用的认证器,这些认证器永远不会离开设备,这是最安全的解决方案。具体来说,基于 RSA 或 ECDSA 的数字签名,如第二章所述,可以用来从随机挑战生成认证响应。在这种情况下,验证方仅需要相应的公钥来检查返回的签名的有效性。
注意
在大多数情况下,认证仅可能通过秘密信息来实现。因此,保密性是所有类型认证器的自然保护目标。如果被破坏,设备冒充便成为可能的威胁。
专用认证芯片
如第五章所述,半导体制造商提供多种认证芯片,这些芯片不仅能够安全地存储认证信息,还提供一种算法手段,进行挑战-响应握手以实现认证。
这种方法有两个优点。首先,从芯片中提取秘密认证信息对于攻击者来说是一项非常困难的任务。其次,由于这些芯片通常集成了支持非对称加密的功能,主要是基于椭圆曲线的数字签名,秘密信息永远不需要离开芯片的物理边界。
另一方面,采用这种方法后,你的物料清单(BOM)上会增加另一个组件,你需要在 PCB 上预留空间,且这些设备的必要软件集成工作在不同厂商之间有所不同。此外,一个常被忽视的攻击向量是——即将此类身份芯片物理转移到另一个设备的过程。简单的 8 脚封装可以被拆焊下来,集成到另一个原始设备中,甚至是定制的攻击者设备中。就像代码提取攻击一样,攻击者可能并不关心芯片内的秘密信息,只要他们能够将整个芯片移到他们想要的位置。
多因素认证
对于人类用户的身份验证,多因素认证 (MFA) 在过去几年里受到了广泛关注。根据深度防御原则,MFA 要求攻击者不仅要获取一个身份验证器,例如密码,还需要至少一个第二因素,比如由移动应用或硬件令牌生成的临时令牌。由于密码存储在人类的大脑中(或密码管理器中),并且额外的身份验证器通常来源于附加的硬件设备或至少是不同的通信渠道,因此成功攻击所需的努力显著增加。
对于设备认证,情况有些不同,因为设备不会像人类大脑或移动应用那样存储和生成身份验证器。然而,你仍然可以考虑多因素方法——例如,使用存储在固件中的一个身份验证器,以及一个来自专用认证芯片的第二身份验证器。认证过程将包含两个握手,一个是与硬件组件的握手,另一个是基于设备软件的握手,迫使攻击者如果想要窃取设备身份,必须破坏设备的两个不同部分。
除了额外的显式身份验证器,你还可以使用隐式的环境参数来加强设备认证。一个常见的例子是地理限制,也叫地理围栏。在这种情况下,设备认证(或一般操作)只有在设备的位置与预定义区域匹配时才会成功。确定此参数的一种方法是设备用于互联网通信的公共 IP 地址。当然,利用这些隐式身份验证属性的安全性得到了最大化,如果攻击者已经侵入设备,无法伪造这些参数。它们应该是从外部可观察的,而不仅仅是设备本身声明的。
受信第三方
过去,设备身份的主要验证者是同一设备的制造商。专有的(最终不安全的)身份验证过程完成了它们的工作。然而,在物联网和工业物联网(IoT 和 IIoT)场景下的多方数字生态系统中,跨制造商设备认证的需求变得非常明显。
这一要求意味着制造商必须信任其他设备的身份验证器,包括竞争对手。由于制造商之间的一对一信任关系会导致巨大的管理开销,因此必须引入受信第三方(TTP)的概念,如图 6-2 所示。

图 6-2:受信第三方在设备认证中的角色
在这种方法中,制造商将其设备身份注册到 TTP ➊。在验证之后,TTP 认证给定的身份并返回一个设备特定的证书 ➋。在现场的身份验证请求中 ➌,设备可以提供颁发的证书并通过加密证明其拥有相应的验证信息 ➍。然而,此时,验证方无法确定给定的加密数据是否对应实际设备身份。验证者最终必须检查认证的有效性 ➎,可以通过直接与 TTP 通信或使用 TTP 提供的公钥等数据来完成。之后,可以建立与先前未知设备的可靠信任关系。
证书和证书授权机构
我一直在使用证书这个术语来描述由 TTP 颁发的数字文档,用以确认设备的身份。从技术角度讲,最常见的实现方式是基于非对称加密的 X.509 v3 证书,如 RFC 5280 中所规定的那样。
这些证书的目的是将给定的公钥绑定到其对应的主体上,例如一个设备,并将其与一组属性(包括有效期和证书序列号)关联起来。证书授权机构(CA)用其自己的私钥对这些值进行数字签名。这个 CA 也包含在证书中,位于Issuer字段中。结果是最小的证书链版本,这意味着设备证书及其公钥可以通过加密方式进行验证,如果验证成功,则需要验证下一个证书(即 CA 证书)。只有当两个验证都成功时,认证才是可信的。
实际上,制造商可能拥有自己的产品 CA,这个 CA 由 TTP 的一个中间 CA 认证,而该中间 CA 又由一个国际公认的根 CA 认证。通过这个过程,建立了相当复杂的层次化证书链,每当设备需要进行身份验证时,都必须验证到根证书。
根证书没有经过任何机构的认证;它们是自签名的,必须能够在验证方的一种根存储中找到。这意味着验证方也必须无条件地信任所有的根证书。因此,根存储需要强大的完整性保护;否则,攻击者可以通过篡改存储的证书来注入新的信任关系。
在某些情况下,证书可能直到其有效期结束之前都无法被信任——例如,由于私钥泄露、设备被盗或类似问题。针对这种情况,证书授权机构(CA)维护一个证书吊销列表(CRL),列出所有即使在其有效期尚未结束的情况下,也不再被信任的证书。在线证书状态协议(OCSP)是一个常用的协议,用于在认证过程中检查证书的吊销状态,已在 RFC 6960 中标准化。
整个验证、认证和吊销的架构,以及相应的流程和服务,通常被称为公钥基础设施(PKI)。由于这种系统需要大量的维护和文档工作,小型和中型企业通常会犹豫是否自己实施,而是依赖于 PKI 服务提供商,这就意味着信任第三方(TTP)。
身份生命周期与管理
现在我们已经介绍了设备认证的基本概念,本节将建立管理设备身份的生命周期的可靠策略需求。生命周期管理有四个主要步骤,如图 6-3 所示:身份生成、在制造商系统及设备中的配置、在现场的日常使用,以及经常被忽视的身份交换或销毁。

图 6-3:设备身份的生命周期
警告
不要将生命周期管理视为可选项。即使你已经解决了标识符、加密技术和安全内存等所有技术难题,也要确保你的组织已经准备好应对未来的组织性挑战。
生成
设备的身份可以在不同的地方和不同的时间生成。你选择的地点和时间会影响生产过程中安全要求和流程。如果你使用电子制造服务(EMS)来制造产品,与服务提供商的信任和密切合作至关重要。
在生产过程中直接在设备上生成身份可能是最安全的选项,但前提是相关的认证密钥永远不离开设备。非对称加密技术支持这种用例,因为生成的私钥可以保留在设备上,而其公钥可以提供给潜在的验证者。当然,你也可以在生产过程中生成对称密钥,但在这种情况下,密钥必须在后期导出以启用身份验证。
虽然设备端生成具有安全优势,但也带来了操作和实际的挑战。想象一下,如果一个设备“丢失”了其身份,因为存储身份的内存损坏。如果这是唯一的存储位置,修复后必须重新生成身份,这可能会导致冲突,因为在您的产品数据库中生成了一个新的条目,但硬件实际上是旧的。此外,您的客户必须在他们的资产管理系统中用新的身份替换旧的设备身份。如果您作为制造商有身份备份,这种情况可以轻松处理,但代价是安全性。
设备端生成的另一个缺点是产品身份的延迟可用性,因为它们只有在某个生产步骤完成后才能获得。有时这可能正是您所需要的,但如果您的设备身份必须在您自己的 IT 系统中填充,以便从第一天起顺利运营,那么您可能希望在实际生产之前就准备好这些设备身份相关的过程。
警告
基于 RSA 密钥的设备端身份生成是一个非确定性过程,所需时间不固定。在规划生产过程中必须考虑这一限制,尤其是对于低性能设备。
在设备外生成身份提供了更多的灵活性,可以在生产之前和修复过程中管理设备身份。认证密钥提前在身份管理系统中准备,并在第二步提供给生产。然而,这意味着这些身份在真实设备组装之前就已经存在,并且当然它们已经承担了保密性和完整性的保护目标。在生产前的信息泄露或数据篡改可能对您的产品安全产生严重后果。
另一个可能影响您选择设备端或云端生成的因素是第三方信任方(TTP)的介入。如果您在生产过程中生成身份,则必须在严格的时间安排内与第三方进行注册、验证和认证。当然,这是可能的,也是理想的,且这一做法已被该领域的领先者实施,但它需要大量的基础设施和流程管理工作。
配置
根据身份生成的阶段,以下的配置步骤有两种形式,各自有优缺点。在这两种情况下,最终结果应该是身份被配置到设备本身,并且在制造商和最终涉及的 EMS 提供商的产品跟踪和身份管理系统中。
在设备上生成身份后,所有制造商系统都必须通过生产过程中的读取步骤来进行身份配置。对于非对称加密,只需将公钥或来自 TTP 的相应证书存储在制造商的身份数据库中。然而,如果需要备份认证密钥,你可以通过此时提取私钥来创建备份。
离线生成身份需要信息流向另一个方向——即从身份管理系统到待生产的设备。显然,需要一个编程步骤,将预生成身份的密钥和属性写入产品内部的特定存储位置或硬件资源。这一步骤可能会被集成到现有的固件编程流程或类似的过程当中。
在所有这些情况下,当敏感数据在设备配置过程中传输时,至少应该保证该通信的完整性,通常还应保证其保密性。否则,认证密钥可能会被泄露,设备可能会拥有被篡改的身份,或者制造商的身份数据可能会被损坏。
注意
如果你的设备身份在生产之前生成,然后通过电子邮件或邮寄的 USB 闪存驱动器发送给你的 EMS 提供商,仔细考虑一下这是否达到了你的保护目标。如果你诚实地思考,可能并没有。
现场使用
之前生成和提供的身份用于现场认证。到目前为止,一切正常。那么我们在日常使用中能采取其他预防措施吗?当然可以。身份管理系统使我们能够在设备与我们的系统进行认证时,执行合理性和可信度检查。
想象一下,你的认证日志显示同一个设备在短时间内从两个不同的地点连接。这可能是某人偷走了设备的身份并出于自己的目的使用它的迹象。如果能及早识别出这样的情况,并及时进行具体调查,损失可以得到显著的控制。
交换或销毁
即使某些设备(尤其是在工业、军事或太空应用中)在物理上设计为可以永久使用,它们的认证密钥通常不会。互联网上,Web 服务器证书的常见有效期是 90 天(例如,Let’s Encrypt 在 letsencrypt.org 实现了这一点),这意味着这些身份至少每三个月就需要重新生成一次。
显然,在 IoT 和 IIoT 场景中的身份更新仍远未达到如此高的频率。然而,至少如果使用 X.509 证书进行身份验证,那么有效期就是一个必须指定的参数,无论是由贵公司还是由您选择的 TTP 指定。一些制造商发放的设备证书有效期长达 20 年或更长,但即使所选加密算法是面向未来的,仍难以估计 15 年或更长时间后该身份是否仍然值得信赖。
一些网络产品(例如思科的产品)支持像 简单证书注册协议(SCEP) 或其较新的替代方案 通过安全传输注册(EST) 这样的证书管理协议。由于这是 IoT 和 IIoT 设备的全新领域,直到撰写本文时尚未建立通用标准,但很明显,自动化是持续可靠的身份和证书管理的关键。
注意
2022 年,德国健康远程医疗基础设施的安全网关制造商声称,由于其五年期加密身份的有效性已到期,这些设备必须进行物理更换。随后,混乱计算机俱乐部(CCC)证明了相反的观点,并根据其说法节省了德国医疗保健系统 €4 亿。这只是一个例子,强调了强大的身份更新过程的重要性。
身份生命周期的最终步骤实际上是它的销毁。虽然物理移除并不总是可行,但制造商至少应该准备好撤销特定设备的信任关系,如果该设备在定义的有效期结束之前已经达到其使用寿命。为此目的,典型的措施是由 CA 维护的 CRL 或在制造商数据库中设置的信任状态标志。
案例研究:身份生成与配置
在本案例研究中,我将研究 STM32-MP157F-DK2 的标识符可用性及如何提取它们以推导出系统身份。此外,我们还将了解如何在该设备上准备证书签名请求(CSR),该请求随后可以提供给 TTP,TTP 进而能够颁发有效的设备证书。
标识符和系统身份
STM32MP157F-DK2 评估套件是一个嵌入式系统,包含多个组件。这些组件中的许多自带标识符,工程师可以捕获并使用它们来创建全面的设备身份。
一个常见的标识符是设备主 CPU 的序列号。在这方面,ST 的 参考手册 RM0436 中提到: “96 位唯一设备标识符提供一个参考编号,在任何上下文中对给定设备都是唯一的。用户不能更改这些位。” 这个唯一 ID (UID) 被不可更改地存储在 STM32-MP157F 芯片的 OTP 内存中。列表 6-1 显示该 UID 被分为三个 32 位字,可以从特定的内存地址读取。
Base address: 0x5C00 5000 (BSEC base address on APB5)
Address offset: 0x234 = UID[31:0]
Address offset: 0x238 = UID[63:32]
Address offset: 0x23C = UID[95:64]
列表 6-1:STM32MP157F 设备中 UID 的物理地址
我们可以使用 devmem2 命令行工具来读取物理内存地址。如列表 6-2 所示,应用程序输出三个 32 位字,表示芯片的身份,通过基地址和 UID 偏移的组合来获取。
# devmem2 0x5c005234
...
Read at address 0x5C005234 (0xb6fb0234): 0x0038003D
# devmem2 0x5c005238
...
Read at address 0x5C005238 (0xb6fb9238): 0x34385114
# devmem2 0x5c00523c
...
Read at address 0x5C00523C (0xb6f1423c): 0x36383238
列表 6-2:从物理地址读取我的 STM32MP157F 设备的 CPU UID
在 Linux 系统中,序列号也可以从 /proc/cpuinfo 获取。列表 6-3 中的输出确认序列号与之前从原始内存位置提取的序列号相同。
# cat /proc/cpuinfo | grep Serial
Serial : 0038003D3438511436383238
列表 6-3:捕获在 Linux 中可用的 CPU 序列号
然而,STM32MP157F 芯片并不是 PCB 上唯一的芯片。ST 的 用户手册 UM2637 描述了多种实现的通信接口。除了经典的以太网网络,设备还包括一个提供 Wi-Fi 和蓝牙功能的 IC。所有这些接口都有唯一的 MAC 地址,可能会用来推导系统身份。列表 6-4 显示了在 Linux 上运行时如何提取这些值。
# cat /sys/class/net/eth0/address
10:e7:7a:e1:81:65
# cat /sys/class/net/wlan0/address
48:eb:62:c4:0a:08
# cat /sys/kernel/debug/bluetooth/hci0/identity
43:43:a1:12:1f:ac (type 0) 00000000000000000000000000000000 00:00:00:00:00:00
列表 6-4:在 Linux 中提取以太网、Wi-Fi 和蓝牙 MAC 地址
最后,系统的一个部分可以轻松地移除和替换:可移动介质卡。在我的例子中,它是一个包含 卡片识别 (CID) 的 microSD 卡。这个 128 位的值唯一标识一张 SD 卡。它包含制造商 ID、产品序列号以及生产日期等信息。再次强调,Linux 提供了一个对应的条目,在 sysfs 中可以读取,正如列表 6-5 所示。
# cat /sys/block/mmcblk0/device/cid
275048534431364760dad3df9a013780
# cat /sys/block/mmcblk0/device/serial
0xdad3df9a
列表 6-5:读取 SD 卡的唯一 CID
除了 cid 值,Linux 还提供了 SD 卡的 serial 值,该值仅包含存储卡的序列号。
对于本案例研究,假设您的团队选择使用中央 CPU ID 和 Wi-Fi MAC 地址作为两个相关的系统标识符。它们可以通过哈希函数组合在一起,如下一节所示。
证书签名请求
证书签名请求(CSR) 是一种数据结构,要求 CA 认证某个公钥与特定身份绑定,在本例中即设备身份。Linux 提供了多种方法来生成 CSR 并提供必要的信息。清单 6-6 展示了通过 cryptography Python 模块实现 RSA 密钥生成和 CSR 创建所需的导入。同时,subprocess 模块也被引入,用于通过系统命令行工具获取系统标识符。
import subprocess
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
清单 6-6:来自 cryptography 和 subprocess 模块的必要导入
设备上身份生成的第一部分通常基于非对称加密(在本例中是 RSA)。如清单 6-7 所示,可以通过一行代码生成一对随机密钥。
# Generate RSA key
key = rsa.generate_private_key(public_exponent=65537, key_size=4096 ➊)
# Write key to disk
with open('dev.key', 'wb') as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=
serialization.BestAvailableEncryption(➋ b'PrivateKeyPassphrase'),
))
清单 6-7:设备上生成 RSA 密钥对
对于本案例研究,我决定使用 4,096 位的 RSA 密钥长度 ➊,以考虑到 (I)IoT 设备的使用寿命为几年。为了简化这个示例,生成的私钥保存在dev.key文件中,并由一个标准的密码短语保护 ➋。在实际的生产环境中,密钥应该以安全的方式存储,如第五章中所讨论的那样。
清单 6-8 展示了标识符收集和处理的示例过程。
# Collect system data
➊ output = subprocess.Popen('cat /proc/cpuinfo | grep Serial',
shell=True, stdout=subprocess.PIPE)
cpu_serial = output.stdout.read().split()2
➋ output = subprocess.Popen('cat /sys/class/net/wlan0/address',
shell=True, stdout=subprocess.PIPE)
wifi_mac = output.stdout.read().split()[0]
# Hash collected system data
➌ digest = hashes.Hash(hashes.SHA256())
digest.update(cpu_serial)
digest.update(wifi_mac)
system_id = digest.finalize()
➍ system_id = system_id[:4].hex()
清单 6-8:设备上标识符的收集和处理
在第一步中,通过 Linux 提供的方式读取生产系统的 CPU 序列号 ➊ 和 Wi-Fi MAC 地址 ➋。随后,使用 SHA-256 哈希函数 ➌ 处理这些值,并派生出一个 4 字节的系统标识符 ➍,如果将来更换 CPU 或 Wi-Fi 芯片,该标识符将会发生变化。故意忽略了 SD 卡 ID,因为 SD 卡时常会损坏,这会导致不必要的身份重新生成需求。
对于设备证书和 CSR,分别需要为设备指定一个通用名称,如清单 6-9 所示。
# Manufacturer data
manufacturer = 'IoT Devices Corp'
manufacturer_device_serial_no = 'IOTDEV-1337-08151234'
# System name for CSR and certificate
➊ cert_common_name = manufacturer_device_serial_no + '-' + system_id
# Generate CSR and sign with private key
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, manufacturer),
➋ x509.NameAttribute(NameOID.COMMON_NAME, cert_common_name),
➋ ])).sign(key, hashes.SHA256())
# Write CSR to disk
with open('dev.csr', 'wb') as f:
f.write(csr.public_bytes(serialization.Encoding.PEM))
清单 6-9:设备上 CSR 准备过程
在本案例研究中,唯一的设备名称是由制造商提供的序列号和硬件相关的系统标识符 ➊ 组合而成的。这串字符串被用作 CSR 生成的输入 ➋,并与制造商名称一起出现在 CSR 的组织字段中。最后,设备用其唯一的私有密钥 ➌ 签署 CSR。之后,CSR 被保存在dev.csr文件中。
保存的 CSR 文件必须传输到负责认证生产设备身份的 CA。此外,制造商或 EMS 提供商可能会从数据库中提取已收集和生成的设备数据。例如,清单 6-10 展示了来自 STM32MP157F 设备的数据。
Collected CPU serial number: 0038003D3438511436383238
Collected Wi-Fi MAC address: 48:eb:62:c4:0a:08
Derived system identifier: f30cf858
Given device serial number: IOTDEV-1337-08151234
Common name in certificate: IOTDEV-1337-08151234-f30cf858
清单 6-10:来自我的 STM32MP157F 设备的标识符数据示例输出
如您所见,一个 4 字节的系统标识符是从列出的各个标识符生成的,并附加到设备序列号后面。这个字符串随后作为生成的 CSR 的常见名称。
证书授权机构
在我们颁发最终证书之前,先看看 CSR 包含了什么。清单 6-11 展示了如何使用 openssl req 命令行工具显示 CSR 内容。
$ openssl req -in dev.csr -noout -text
Certificate Request:
Data:
Version: 1 (0x0)
➊ Subject: O = IoT Devices Corp, CN = IOTDEV-1337-08151234-f30cf858
➋ Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:d3:a0:14:fb:e1:0e:d0:74:3d:26:d4:ef:a1:ed:
...
c9:2a:f5:46:e4:b2:ad:a9:5e:ee:cb:79:85:d9:1e:
9f:3e:57
Exponent: 65537 (0x10001)
Attributes:
(none)
Requested Extensions:
Signature Algorithm: sha256WithRSAEncryption
➌ Signature Value:
81:98:b1:e8:c2:fe:3a:55:32:39:2e:27:ce:2c:a8:54:bd:04:
...
17:77:6c:a1:5b:4a:a7:ed:22:55:33:23:26:55:05:90:26:d2:
90:7a:5e:34:65:80:32:4e
清单 6-11:我的 STM32MP157F 设备的示例 CSR
主题 ➊ 由组织字符串(O)和常见名称(CN)表示,如我们在清单 6-9 中的 CSR 准备脚本所指定,接着是相应的 RSA 公钥 ➋。设备的数字签名 ➌ 可以在给定请求的末尾清楚地识别出来。CA 可以使用它来验证请求主体是否确实拥有与 CSR 中给定公钥对应的私钥。
CA 和 PKI 基础设施通常由复杂的流程组成,采用各种组织和技术措施以确保其正常可靠地运行。如清单 6-12 所示,我们创建了一个测试 CA,它远未准备好用于生产环境,但对于教育目的来说是可以的。这里或许可以使用 快速且简陋 这一术语。
$ openssl genrsa -out ca.key 4096
$ openssl req -new -x509 -key ca.key \
-subj "/C=DE/L=Augsburg/O=Super Trusted Party/CN=CA 123" \
-out ca.crt
清单 6-12:使用 openssl 工具快速生成测试 CA
我们可以借助 openssl genrsa 工具生成测试 CA。清单 6-12 中的第一条命令为 CA 生成一个 4096 位的 RSA 密钥对,并将其存储为 ca.key。因为在这个案例研究中,它是 CA 的根证书,所以相应的证书必须是自签名的。可以使用 openssl req 工具,并告诉它 CA 的属性——例如,国家(德国为 DE)和所在城市(Augsburg),它的组织名称(Super Trusted Party),以及它的常见名称(CA 123)来获取 ca.crt 证书。
在 CA 注册并成功验证当前证书请求后,它会获取 CSR 数据,并添加有效期等属性。在清单 6-13 中,您可以看到 -days 参数被设置为 3650,这意味着颁发的证书有效期为 10 年。
$ openssl x509 -req -in dev.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 3650 -out dev.crt
清单 6-13:使用 openssl 工具从 CSR 生成证书
在设备证书生成过程中,CA 决定有效期的长度,但当然,这会影响您的设备身份生命周期。确保仔细选择此值。
让我们看看这一复杂过程的最终结果。openssl x509 工具可以输出设备证书内容,如清单 6-14 所示。
$ openssl x509 -in dev.crt -noout -text
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
➊ 45:3c:c3:30:c1:e3:c2:a9:49:5c:14:d6:16:5d:79:69:24:6c:31:66
Signature Algorithm: sha256WithRSAEncryption
➋ Issuer: C = DE, L = Augsburg, O = Super Trusted Party, CN = CA 123
Validity
Not Before: Apr 5 11:18:13 2024 GMT
➌ Not After : Apr 2 11:18:13 2034 GMT
Subject: O = IoT Devices Corp, CN = IOTDEV-1337-08151234-f30cf858
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
00:d3:a0:14:fb:e1:0e:d0:74:3d:26:d4:ef:a1:ed:
...
c9:2a:f5:46:e4:b2:ad:a9:5e:ee:cb:79:85:d9:1e:
9f:3e:57
Exponent: 65537 (0x10001)
Signature Algorithm: sha256WithRSAEncryption
➍ Signature Value:
75:d5:07:71:ec:fe:c6:27:fd:e2:a7:1c:fa:b9:89:b3:9c:0f:
...
8d:fa:f6:f1:53:79:32:1e:a8:ec:6f:f7:03:57:2f:7b:f4:fb:
45:77:6a:f8:c6:70:72:41
清单 6-14:示例设备的证书内容
与原始的 CSR 相比,你可以看到 CA 在Issuer字段中添加了证书序列号➊和自己的数据➋。有效期➌被设置为从颁发时起的 10 年。最后,所有这些属性由 CA ➍与设备信息及其公钥一起签名。现在,每个信任所用 CA 的实体都能够验证生成的设备。
在颁发证书后,证书必须提供给设备本身,也需要提供给制造商的身份管理系统。在生产过程中,整个生成、证书颁发和配置的过程应具备高程度的自动化,并采取预防措施以最小化对机密性和完整性的威胁。
案例研究:生产中的 RSA 密钥生成
尽管 ECDSA 相较于 RSA 具有一些优势,如第二章所讨论,但它仍然广泛应用于证书中。然而,如果你使用 RSA,必须注意 RSA 密钥生成是一个非确定性过程,可能需要不同的时间。
这个简短的案例研究探讨了在生产过程中生成给定长度的 RSA 密钥所需的时间。清单 6-15 展示了一种分析 RSA 密钥生成时间的简单方法。
import time
from cryptography.hazmat.primitives.asymmetric import rsa
time_data = []
for n in range(16):
start_time = time.time()
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
elapsed_time = time.time() - start_time
print('Try', n, ': RSA 4096-bit key generation took',
'{:.3f}'.format(elapsed_time), 'seconds!')
time_data.append(elapsed_time)
print('MIN:', '{:.3f}'.format(min(time_data)), 'seconds')
print('MAX:', '{:.3f}'.format(max(time_data)), 'seconds')
print('AVG:', '{:.3f}'.format(sum(time_data)/len(time_data)), 'seconds')
清单 6-15:RSA 密钥生成时间分析
本例使用了cryptography Python 模块,并使用了前一个案例研究中的参数。为了简单起见,它进行了 16 次尝试,但一个合理的统计分析需要更多的测试运行。清单 6-16 展示了通过在我的 STM32MP157F 设备上运行清单 6-15 中的代码,获得的 RSA 4,096 位密钥生成时间的示范结果。
# python3 rsa_key_gen_time.py
Try 0 : RSA 4096-bit key generation took 59.920 seconds!
Try 1 : RSA 4096-bit key generation took 28.696 seconds!
Try 2 : RSA 4096-bit key generation took 72.872 seconds!
Try 3 : RSA 4096-bit key generation took 109.765 seconds!
...
Try 12 : RSA 4096-bit key generation took 48.925 seconds!
Try 13 : RSA 4096-bit key generation took 50.885 seconds!
Try 14 : RSA 4096-bit key generation took 90.907 seconds!
Try 15 : RSA 4096-bit key generation took 40.634 seconds!
MIN: 28.696 seconds
MAX: 109.765 seconds
AVG: 62.768 seconds
清单 6-16:在我的 STM32MP157F 设备上进行的 RSA 密钥生成时间结果
生成时间的变化不可忽视。RSA 密钥生成可能在 30 秒内完成,但也可能需要 110 秒甚至更长时间。这个变化必须考虑到生产调度中,并且由于生成时间没有上限,你必须预期到可能会出现需要显著更长时间的异常情况。
总结
毋庸置疑,每个设备都是一个物理上独特的对象。借助 CPU 序列号、MAC 地址以及制造商选择的值等标识符,我们能够在数字空间中表示这种独特性,并为设备身份提供基础。
然而,单纯声明身份对于大多数应用来说并不足够。设备必须能够借助独特且保密的认证器,如加密密钥,来加密地证明其身份。这个过程被称为身份验证。安全存储这些认证秘密对于防止冒充攻击至关重要。第五章提供了一些在硬件或软件中存储机密数据的思路。
建立设备身份信任的一个常见概念是将设备注册到第三方,第三方验证其身份并颁发数字设备证书。这些证书可以被任何信任颁发者的人用来验证设备的身份。
除了将数字身份绑定到设备的技术挑战外,还需要规范更广泛的组织流程,以提供安全可靠的身份生命周期管理。这些流程通常涉及 EMS 提供商、可信第三方(TTP)以及您的定制流程细节,这导致了一个不容忽视的复杂性。
你越深入研究这个话题,就会发现越多“有趣”的问题。研究人员多年来一直在致力于 PUF(物理不可克隆函数)实现,利用制造过程中的差异来推导隐式芯片身份,市场上的首批产品已经包含了这种电路。此外,设备身份管理的自动化,在在线和离线场景中的相应协议(如 SCEP 和 EST)无疑将在未来获得更多关注,为管理安全设备身份提供了重大进展。
第七章:安全通信

过去,许多嵌入式系统工作在隔离网络环境中,既不连接有线网络,也不连接无线网络。尽管这种做法在某些行业中仍然存在,但它正在慢慢消失,原因很简单,现代的一些理念,如预测性维护、数据驱动优化和远程访问,若没有适当的通信渠道,是无法实现的。
甚至在凯撒时代,最早的加密方法也旨在保护通信,2000 年后的今天,互联网在没有安全通信的情况下是不可想象的。然而,仍然有相当数量的设备在与管理员及其周围其他实体的数据交换中,未使用安全协议。
本章中,我们首先将探讨对稳健通信渠道的要求,接着介绍应对这些挑战的最常见协议,并提供一个相应的实践案例研究。随后,我们将讨论标准解决方案无法解决的两个领域:非 IP 通信和冗余需求。
所有保护目标
交流是人类天性的一部分。我们交换思想,提供帮助,并在团队中合作。每个低声在别人耳边说话的孩子,都隐含地意识到保密的重要性。
对于书面信息,保密性已经是一个讨论了几千年的话题,如凯撒的例子所示,并且它仍然存在于邮政隐私法中。几个世纪以来,君主们使用皇家印章来保证文件的完整性和真实性,而通过活字印刷复制信息显然是为了确保可用性——即使某个“信息源”失败,仍然可以通过其他方式获取相同的“通信数据”。
数字通信在互联网规模上已经实践了大约 30 年,毫不奇怪,它集成了以往通信方法的所有保护目标的需求。考虑到(I)IoT 设备通信的特定领域,保密性显然是有用的,因为传输的可能是与知识产权相关的专有数据,或是需要隐私保护的个人数据。
同时,完整性和真实性在许多情况下也是非常重要的——例如,在工业系统中的控制命令。在这些场景中,确保通信的来源是合法方,并且信息在传输过程中没有被篡改,非常重要。如前所述,典型的(I)IoT 设备依赖于正常工作的通信渠道。干扰——例如,DoS 攻击引入的干扰——威胁到系统的正常运行,甚至可能危及相关的商业模式。
传输层安全性
当我们谈论数据通信时,我们通常从开放系统互联(OSI)模型中的位置开始。这个模型帮助我们构建通信协议栈,如表 7-1 所示。
表 7-1: 根据 OSI 模型的通信层
| 编号 | 层级 | 描述 |
|---|---|---|
| 7 | 应用层 | 特定应用的通信数据处理 |
| 6 | 表示层 | 网络数据与应用之间的转换 |
| 5 | 会话层 | 节点之间通信的会话管理 |
| 4 | 传输层 | 网络节点之间数据传输的管理 |
| 3 | 网络层 | 多参与者网络的管理 |
| 2 | 数据链路层 | 两个实体之间的数据帧传输 |
| 1 | 物理层 | 通过物理介质进行有线/无线传输 |
以太网标准 IEEE 802.3 是一个典型的例子,规定了第 1 层和第 2 层的属性。通常,互联网协议(IP)处理网络层,而传输控制协议(TCP)和用户数据报协议(UDP)覆盖第 4 层和第 5 层的功能。像常见的超文本传输协议(HTTP)这样的应用协议由第 7 层表示。
本节重点介绍一种最常见的安全通信协议:传输层安全协议(TLS)。看似显而易见,它位于 OSI 模型的第 4 层;然而,加密和解密通常归属于第 6 层。由于 TLS 还执行某种会话管理,因此我们可以说它跨越第 4 到第 6 层,从而在网络上分发的原始数据包与最终使用传输数据的应用之间引入了一个保护层。
注意
TLS 与应用无关。它的有效载荷可以携带任何应用协议,如 HTTP 或甚至工业协议,如 Modbus。
历史
在 1990 年代,当 Netscape 开发同名浏览器应用程序时,安全互联网通信的需求变得显而易见。1995 年,Netscape 发布了 TLS 的前身——安全套接层(SSL)协议 2.0 版本。SSL 2.0 和随后发布的 SSL 3.0 版本存在严重的安全问题,绝不应该在实际中使用。
不幸的是,许多软件应用程序和产品营销手册将SSL和TLS同义使用。通常,您可以假设它是“现代 TLS 版本”,但作为客户,如果制造商不再提及 SSL,我会更有信心,认为其展示了更高的安全技术水平。
新的协议名称在 1999 年随 TLS 1.0(这是 SSL 3.0 的升级版本)推出,可能是为了减少与之前 SSL 版本漏洞的关联。TLS 1.1 在 2006 年修复了基于块加密的 CBC 操作模式中的安全问题。然而,TLS 1.0 和 TLS 1.1 在 2021 年 3 月正式弃用,不应在现代产品中使用。
TLS 1.2 版本于 2008 年 8 月发布,至今仍被广泛使用。它用现代对等算法 SHA-256 替代了 MD5 和 SHA-1 等弱哈希函数,并扩展了对 AES-GCM 等认证加密密码的支持。然而,TLS 1.2 的复杂性反过来成了它的敌人,正确的配置并非易事。为了最小化配置错误,同时最大化安全性和性能,TLS 1.3 于 2018 年发布。每个新设备应默认使用该版本。
TLS 基础知识
TLS 是一组客户端-服务器协议,它统一了第二章中引入的许多现代加密原语。这些是实践中最重要的两个子协议:
握手协议 在此子协议中,用于保护通信通道的加密算法在客户端和服务器之间进行协商。通常,其中至少一个会经过认证与对方通信,但也可以进行相互认证。此外,握手过程还建立了用于随后的通信中的加密算法的共享密钥材料。此阶段的任何错误或篡改攻击都会导致连接的终止。
记录协议 此子协议负责基于在握手协议中协商的算法和参数,组织和保护两个端点之间的大部分流量。
TLS 的另一个通用特性是它使用 X.509 证书进行身份验证。然而,与第六章中提到的设备证书不同,这些证书中的通用名称通常对应于设备的 IP 地址、主机名或完全限定域名(FQDN)。这是因为这些信息用于首先在网络层建立与设备的基本连接,然后设备必须证明它是该网络节点上的合法实体。
永远不要低估 TLS 的复杂性,当没有具体要求更改时,请坚持使用安全的默认设置。以下章节阐明了当前使用的两种 TLS 版本的主要特性。
TLS 1.3
TLS 1.3 在 RFC 8446 中进行了规范,它是产品工程师应当实现和使用的版本。图 7-1 展示了相互认证的握手过程,例如,这可能是机器对机器通信场景的一部分。

图 7-1:典型的 TLS 1.3 握手过程,带有相互认证
在第一步中,客户端通过向服务器发送ClientHello ➊消息来发起连接建立。此消息包含客户端的密钥共享,用于 DHE 或 ECDHE 密钥交换方案、其支持的 TLS 版本以及可用的加密算法和参数列表。作为回应,服务器提供其用于预期密钥交换的数据、其证书,以及证明其拥有相应私钥的签名。此外,它还要求客户端进行身份验证,并以Finished消息结束握手中的ServerHello ➋。
在握手的第三部分,客户端通过发送其证书及证明其身份的签名来满足服务器的证书请求 ➌。在客户端的Finished消息之后,双方可以确认对方的身份,并准备安全地交换应用数据 ➍。
进一步的握手选项
除了如 DHE 和 ECDHE 等保证完美前向保密性但也消耗显著性能的密钥交换方案,TLS 1.3 还提供了预共享密钥(PSK)选项,它在早期过程将密钥分发给设备。然而,这不是默认选项,应该仅在特殊情况下并有充分理由时使用。
为了减少执行完整握手的必要性,TLS 1.3 允许在客户端和服务器达成一致的情况下重用来自先前(EC)DHE 握手的预共享密钥(PSK),这意味着可以避免耗费成本的非对称加密操作,从而提高效率。
如果客户端和服务器共享 PSK,TLS 1.3 提供了零往返时间(0-RTT)模式,允许客户端在其第一条消息中发送 PSK 加密的数据,立即启用应用数据通信。然而,这种速度提升是有代价的:无法再保证完美前向保密性,且 0-RTT 消息没有受到早期获取的消息重放保护。
加密算法选择
与 TLS 1.2 相比,TLS 1.3 显著减少了可使用的加密算法集。这是一个强有力的安全增强,因为它防止了降级攻击,这种攻击试图影响加密参数协商,以迫使使用弱算法。在这一清理过程中,密码套件,即支持的加密算法集合,被简化为仅包含对称加密算法。
对于对称加密,TLS 1.3 要求实现 TLS_AES_128_GCM_SHA256 密码套件,这意味着 AES 必须使用 128 位密钥并在 GCM 模式下运行,同时必须提供 SHA-256 哈希函数,用于作为基于 HMAC 的提取与扩展密钥推导函数(HKDF),这是推导 TLS 密钥所必需的。额外的 TLS_AES_256_GCM_SHA384 和 TLS_CHACHA20_POLY1305_SHA256 密码套件也应该实现,但不是强制的。基于 CCM 模式的两个密码套件完成了 TLS 1.3 中对称加密的五种可能选择,它们都属于现代 AEAD 算法类。
关于数字签名算法和证书,TLS 1.3 实现必须支持与 SHA-256 结合使用的 RSA PSS 和 PKCS1,也支持基于 NIST 曲线 secp256r1 和 SHA-256 的 ECDSA。ECDHE 的强制曲线也是 secp256r1,但 RFC 推荐额外实现 X25519。基于有限域群的 DHE 也是可能的。
TLS 1.2
尽管 TLS 1.3 是最新版本,但许多设备仍依赖于 RFC 5246 中规定的 TLS 1.2,或者至少支持其使用,以保持与旧版设备的兼容性。然而,这需要采取预防措施以确保安全通信。
TLS 1.2 与 TLS 1.3
TLS 1.2 和 TLS 1.3 之间的一个正式区别是,1.2 版本的密码套件包括非对称和对称算法及其参数。例如,TLS_DH_RSA_WITH_AES_256_CBC_SHA384 密码套件指定使用 DH(没有 E 表示 临时)进行密钥交换;认证基于 RSA 证书;使用 256 位密钥的 AES 在 CBC 模式下进行负载加密,SHA-384 是用于密钥推导方案的哈希函数。
除了密码套件格式的不同,TLS 1.2 还支持并允许使用更多的加密算法,这在某些情况下可能带来负面影响。除了可以选择基于 RSA 和 DH 的静态密钥交换机制外,甚至可以选择显式允许匿名 DH 密钥交换的密码套件,这意味着不对通信伙伴进行任何验证。
TLS 1.2 还允许选择旧版加密算法,如 3DES 和 Rivest Cipher 4 (RC4),甚至选择 NULL 密码套件,该密码套件不进行负载加密。此外,还可以选择如 CBC 这样的操作模式,这些模式无法提供 AEAD 合规的安全性。
在 TLS 中传输数据前进行压缩的选项出于设计者的良好意图,并在 TLS 1.2 中仍然可用,但它也带来了漏洞,如压缩比信息泄露攻击(CRIME)所示。
除了这些与安全相关的差异外,TLS 1.2 还缺少 TLS 1.3 引入的一些性能优化和 0-RTT 模式。
安全使用
保证 TLS 1.2 安全配置的关键在于严格限制其选项。您的实施必须禁止使用像 3DES、RC4、弱化的出口密码、CBC 操作模式和(当然)NULL密码这样的弃用密码。基于 RSA 密钥传输的密钥交换也必须被拒绝,以及匿名 DH。此外,像数据压缩这样的选项必须被禁用以减少攻击面。
从另一个角度来看,您的配置应仅启用实施 DHE 和 ECDHE 密钥交换以及与 SHA-2 系列哈希函数结合的对称 AEAD 密码套件,这意味着 TLS 1.2 可以被修剪以类似于 TLS 1.3 并提供安全通信通道的方式行事。
设备和基础设施的要求
假设您已经决定 TLS 是您设备的选择,并且希望使事情发生,那么在实施半安全通信混乱之前,您仍然需要考虑一些要求。从设备的角度来看,您至少应该分析五个方面:
私钥存储 如果您的设备需要通过使用私钥对数据进行签名来进行身份验证,则需要一个安全的地方以保密方式存储该秘密。
证书存储 仅当您的设备拥有相应的公钥和根证书时,才能验证通信伙伴的身份。这些文件不包含机密信息,但需要以完整性保护的方式存储,因为它们代表了您设备的信任基础。
可信的随机数源 TLS 中的密钥生成和密钥交换方案需要可信的随机数。如果您的设备基于静态值生成“随机数”,这可能会对 TLS 安全造成严重后果。
可靠的时间基准 尤其是在工业场景中,但也适用于其他应用领域,设备通常不需要实时时钟。但是,如果您想要使用 TLS,则您的设备必须能够验证证书的有效期,这显然在时间上停留在上世纪 80 年代是不可能的。
加密性能 TLS 握手根据非对称加密执行多个操作。低性能设备可能会达到其极限,例如,如果它们必须使用 RSA 密钥进行签名。选择使用 RSA 还是 ECDSA 可能会对此点产生重大影响。如果您的设备被设计为每天进行一次客户端认证,或者作为服务器处理数百个连接和认证请求,那么这也会产生很大的差异。
您的设备本身不仅需要为 TLS 做好准备,您的基础设施和流程也需要支持它。一个常见的例子是操作 PKI 来管理证书的生成、更新和吊销。
此外,您设备中的实时时钟可能依赖于外部时间同步机制,如网络时间协议(NTP)和精密时间协议(PTP),这些机制需要适当的网络服务、主时钟等支持。
应用示例和软件库
TLS 的最常见应用场景是超文本传输协议安全(HTTPS),这是互联网上普遍使用的协议。它也被称为基于 TLS 的 HTTP,因为本质上就是这样:TLS 在客户端和 Web 服务器之间建立了一个安全通道。在这个通道内,交换的是普通的 HTTP 请求和响应。同样的,许多其他应用协议也可以通过这种方式实现安全通信。
TLS 基于 TCP,但是否有办法保护依赖于 UDP 或其他无状态协议的应用,如 VoIP(语音通信)或在线游戏?是的,您可以使用一种变体,称为数据报传输层安全性(DTLS)。DTLS 1.2 和 DTLS 1.3 分别基于 TLS 1.2 和 TLS 1.3,它们保证与其基于 TCP 的对应版本相同的安全性,但能够处理丢包和数据包重排问题。
在 TLS 实现方面,OpenSSL 可能是 Linux 系统以及嵌入式系统领域中最流行的选择。然而,如果源代码透明度或小巧的占用空间对您的产品很重要,Mbed TLS 可能值得一试。此外,还有许多编程语言中的实现可供选择,例如为 Rust 社区提供的 Rustls。
案例研究:安全的 MQTT 通信
在过去的几年里,作为一名教授,我看到许多学生项目使用 MQTT 协议,因为它相当易于理解,且非常适合物联网场景中资源受限设备的需求。它用于传输传感器值,如温度或压力,以及控制消息,如启动和停止命令,用于系统中的执行器。
然而,如果我要求为显然敏感的消息内容提供一个安全的通信通道,通常会收到类似“但这只是一个概念验证!”或“没有时间处理这个复杂的课题。”以及“为什么你总是问这么痛苦的问题?”这样的回答。
在本案例研究中,我将在 STM32MP157F-DK2 开发板上设置 Eclipse Mosquitto MQTT 代理服务,并根据 TLS 配置它以实现安全通信。此外,我将动态测试实现中的配置错误。
Mosquitto 安装与配置
因为我的 STM32MP157F-DK2 设备的工具链是基于 Yocto 项目的,所以我可以简单地将meta-networking层中的recipes-connectivity里的mosquitto配方添加到我的镜像中,以便在设备上安装代理软件。我得到的版本是2.0.14。当然,这只带有位于/etc/mosquitto/mosquitto.conf的默认配置。
在 MQTT 中,代理是一个核心组件,它接收客户端发布的消息和数据,并将这些信息分发给订阅它的客户端。对于客户端来说,至关重要的是仅将数据与合法的代理共享,并且只依赖它们信任的代理发布的消息。因此,MQTT 代理必须通过加密认证来验证连接的客户端。
如前所述,TLS 使用证书进行身份验证,因此代理认证的第一步是生成相应的证书。我使用 Python 和 OpenSSL 以类似于第六章中描述的设备身份的方式,创建了一个 CA 和一个 MQTT 代理证书。一个重要的区别是证书中选择的公共名称:它对应于在我的网络中可以通过http://mqtt.iot-device-corp.com/访问的主机名。这对于客户端的主机名验证至关重要,以便它们能够确保连接到正确的主机。
凭借 CA 证书ca.crt、代理证书mqtt_broker.crt及其对应的私钥mqtt_broker.key,我们可以按照列表 7-1 中所示,配置mosquitto的基本 TLS 设置。
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/mqtt_broker.crt
keyfile /etc/mosquitto/certs/mqtt_broker.key
列表 7-1:mosquitto 的基本 TLS 配置
虽然普通的 MQTT 通常通过 1883 端口提供,但基于 TLS 的安全通信版本通常通过 8883 端口提供。所需的证书和密钥可以存储在/etc/mosquitto/certs/中。例如,至此一切正常。
然而,直到现在,系统仅配置了服务器端认证。客户端认证需要通过在托管代理应用程序的设备上维护一个密码文件来管理(例如,在/etc/mosquitto/password_file),这可能会比较繁琐,且相关的安全级别最多只能算中等。但由于 TLS 支持基于证书的双向认证,而 Mosquitto 能够利用这一过程进行应用层的使用,因此值得一探。
列表 7-2 展示了必须添加到mosquitto.conf的两个选项。
require_certificate true
use_identity_as_username true
列表 7-2:基于证书的 MQTT 客户端认证的重要选项
第一行使代理请求每个连接的客户端提供用于身份验证的证书,而第二行则启用了将提供的证书中包含的公共名称用作 MQTT 应用程序中的用户名。
需要注意的是,在这种情况下,客户端证书必须由cafile参数之前提供的 CA 颁发。如果你想为 Mosquitto 提供多个受信任的 CA,capath选项是你的好帮手。在我的案例中,我使用了与 MQTT 代理相同的 CA 来创建一个额外的证书,这次使用了公共名称mqtt-client123。
第一次测试运行
在使用新配置文件启动mosquitto之后,简短的nmap扫描表明,MQTT 代理现在可以通过端口 8883 访问,如清单 7-3 所示。
$ nmap mqtt.iot-device-corp.com -p 8883
Starting Nmap 7.80 ( https://nmap.org ) at ...
Nmap scan report for mqtt.iot-device-corp.com (192.168.1.13)
Host is up (0.00067s latency).
PORT STATE SERVICE
8883/tcp open secure-mqtt
Nmap done: 1 IP address (1 host up) scanned in 0.03 seconds
清单 7-3:显示开放端口 8883 的nmap扫描
为了测试安全通信,我写了一个小的 Python 脚本,使用了 Eclipse Paho MQTT 客户端库。清单 7-4 显示了基本的设置。
broker = "mqtt.iot-device-corp.com"
port = 8883
client = mqtt.Client("mqtt-client123")
client.tls_set('ca.crt', 'mqtt_client.crt', 'mqtt_client.key')
client.connect(broker, port)
清单 7-4:Python 中的基本 Paho MQTT 客户端配置
除了 MQTT 代理的主机名和端口等明显的必要信息外,客户端还能够处理 TLS 设置,如ca.crt中的受信任 CA,以及位于mqtt_client.crt和mqtt_client.key文件中的客户端认证数据。
在测试应用程序中,客户端订阅主题foo/bar,向同一主题发布一些数据,并再次从代理接收信息,如清单 7-5 所示。
➊ New connection from 192.168.1.7:46317 on port 8883.
➋ New client connected from 192.168.1.7:46317 as mqtt-client123
(p2, c1, k60, u'mqtt-client123').
...
➌ Received SUBSCRIBE from mqtt-client123
foo/bar (QoS 0)
Sending SUBACK to mqtt-client123
➍ Received PUBLISH from mqtt-client123 (d0, q0, r0, m0, 'foo/bar', ... (2 bytes))
Sending PUBLISH to mqtt-client123 (d0, q0, r0, m0, 'foo/bar', ... (2 bytes))
...
Received DISCONNECT from mqtt-client123
Client mqtt-client123 disconnected.
清单 7-5:测试期间 mosquitto 控制台输出
很明显,连接已成功建立在端口 8883 上 ➊。此外,mosquitto直接使用客户端证书的通用名称(mqtt-client123)作为此连接的相关用户名 ➋。两个客户端命令,SUBSCRIBE ➌和PUBLISH ➍,也都正确地在代理端接收到并处理。
使用 Wireshark 和 SSLyze 进行通信安全分析
尽管我们已经启用了多个 TLS 安全特性,但应用程序似乎仍然运行正常。这让人有希望,但作为安全工程师,我们更倾向于通过深入分析而非直觉来建立信任。
进行此类分析的工具之一是 Wireshark。它允许我们捕获网络通信流量并分析其安全性。我首先将其配置为收集测试站和 STM32MP157F 设备之间交换的所有数据。然后,我过滤了 TLS 数据包。图 7-2 显示了结果的片段。

图 7-2:MQTT 通信中交换的 TLS 消息
你可以看到,TLS 握手过程,包括其特征性的ClientHello和ServerHello消息,已经发生。之后,加密的Application Data数据包在客户端和代理之间传输。
如图 7-3 所示,Wireshark 还提供了 TLS 协商的详细信息——即选择了TLS_AES_256_GCM_SHA384密码套件用于大数据传输。这也告诉我们使用的是 TLS 1.3,因为该密码套件属于最新的 TLS 版本。

图 7-3:TLS 握手的详细信息
警告
在分析网络流量中的 TLS 版本时要小心。遗留的 Version 字段显示它是 TLS 1.2,但实际上并非如此。
到目前为止,功能和安全措施似乎按预期工作。让我们再使用一个工具来最终确认我们对这项工作的信心。SSLyze Python 应用程序能够测试各种 TLS 服务器,检查一系列的陷阱和配置错误,如果这些问题被忽视,将会降低产品的安全性。
要启动 TLS 扫描,只需输入主机名和相应的端口:sslyze mqtt.iot-device-corp.com:8883。
综合结果揭示了一些有趣的细节。列表 7-6 中展示的摘录涉及证书验证。
...
Certificate #0 - Trust
➊ Hostname Validation: OK - Certificate matches server hostname
➋ Android CA Store (...): FAILED - Certificate is NOT Trusted ...
Apple CA Store (...): FAILED - Certificate is NOT Trusted ...
Java CA Store (...): FAILED - Certificate is NOT Trusted ...
Mozilla CA Store (...): FAILED - Certificate is NOT Trusted ...
Windows CA Store (...): FAILED - Certificate is NOT Trusted ...
...
列表 7-6: sslyze 关于证书验证的控制台输出
好消息是,我正确创建了证书,这意味着我将设备的正确服务器名称作为证书中的通用名称 ➊。然而,也有许多 FAILED 条目 ➋。之所以出现这种情况,是因为我的证书并不属于 Android、Windows、Mozilla 等常见证书存储库的一部分,这本来不是计划中的事情,但如果你希望证书具有广泛的信任兼容性,那么这个测试对你来说可能很重要。
结果的主要部分是关于服务器接受的加密套件和 TLS 版本的。列表 7-7 中的结果表明,已弃用的 TLS 版本及其相应的加密套件都被实现的 MQTT 代理如预期地拒绝。mosquitto的默认设置似乎已经禁止了这些旧协议和加密算法的使用。
...
* SSL 2.0 Cipher Suites:
Attempted to connect using 7 cipher suites; the server rejected all ...
* SSL 3.0 Cipher Suites:
Attempted to connect using 80 cipher suites; the server rejected all ...
* TLS 1.0 Cipher Suites:
Attempted to connect using 80 cipher suites; the server rejected all ...
* TLS 1.1 Cipher Suites:
Attempted to connect using 80 cipher suites; the server rejected all ...
...
列表 7-7:所需拒绝的所有已弃用加密套件
如列表 7-8 所示,mosquitto的默认设置仍然支持 TLS 1.2,这对于向后兼容性是一个好事,因为许多现场的老旧设备不支持 TLS 1.3,但由 sslyze 给出的受支持加密套件列表则展示了一些弱点。
...
* TLS 1.2 Cipher Suites:
Attempted to connect using 156 cipher suites.
The server accepted the following 20 cipher suites:
TLS_RSA_WITH_AES_256_GCM_SHA384 256
TLS_RSA_WITH_AES_256_CBC_SHA256 256
TLS_RSA_WITH_AES_256_CBC_SHA 256
...
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 256 ECDH: X25519
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 256 ECDH: prime256v1
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 256 ECDH: prime256v1
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 256 ECDH: prime256v1
...
TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 256 DH (4096 bits)
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 256 DH (4096 bits)
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 256 DH (4096 bits)
TLS_DHE_RSA_WITH_AES_256_CBC_SHA 256 DH (4096 bits)
...
列表 7-8:提供的 TLS 1.2 加密算法
输出结果显示,目前的实现仍提供一些选项,允许基于 RSA 的密钥交换(TLS_RSA_WITH_...)和其他仍使用 CBC 操作模式进行 AES 应用数据加密的选项。这两者都不被 TLS 1.3 支持,也不再推荐使用。
这一见解使我们能够再次调整 mosquitto.conf 文件,通过指定 ciphers 参数,如列表 7-9 所示。
ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:
DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:
ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-CHACHA20-POLY1305:
ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:
DHE-RSA-AES128-GCM-SHA256
列表 7-9:mosquitto.conf 中 TLS 1.2 加密算法的限制
这将mosquitto的 TLS 1.2 加密算法限制为仅六个现代的 RSA 认证选项和三个 ECDSA 证书选项,这意味着目前的 MQTT 通信安全性进一步提升。
注意
Mosquitto 依赖 OpenSSL 来实现安全的 TLS 通信。输入 openssl ciphers 以列出 OpenSSL 在特定系统上提供的所有加密套件。
关于 TLS 1.3,推荐的加密套件得到了正确的支持,如列表 7-10 所示。
...
* TLS 1.3 Cipher Suites:
Attempted to connect using 5 cipher suites.
The server accepted the following 3 cipher suites:
TLS_CHACHA20_POLY1305_SHA256 256 ECDH: X25519
TLS_AES_256_GCM_SHA384 256 ECDH: X25519
TLS_AES_128_GCM_SHA256 128 ECDH: X25519
清单 7-10:TLS 1.3 支持按预期可用
这是未来设备通信的基础。
没有 TLS 的安全通信
如本章多次提到的那样,如果你的设备需要进行安全通信,TLS 应该是默认的解决方案。然而,并非所有应用场景和通信技术都允许使用 TLS。
例如,像传感器或网状网络这样的专用无线通信可能依赖于不包含 TCP/IP 栈的专有协议。此外,一些有线总线架构,如控制器局域网络(CAN)总线,定义了自己的消息格式和数据结构,而不依赖于 TCP/IP 技术,并且可能需要某些实时行为,而 TLS 无法保证这些实时需求。而且,一些电池供电的设备,如用于报警系统、车库门和工业起重机的遥控器,通常使用 433 和 868 MHz 的频率,这些频率位于免许可证的工业、科学和医疗(ISM)无线电频段内,它们优化消息内容和长度以减少能耗,从而使 TLS 握手变得不可能。
然而,得出这些应用程序必须在没有安全措施的情况下运行,因为 TLS 不适用,这种结论是天真的。这个问题仅需要一种不同的开发方法。对于这些情况,通常没有现成的解决方案,但必须开发定制的、特定应用的安全协议。当然,拥有丰富的密码学、协议设计和此类系统验证经验将非常有帮助,但实际上,情况往往并非如此。
一种务实的方法是将 TLS 1.3 及其使用的密码学原语视为自助服务商店。如果你需要特定的保护措施(例如,保护遥控器发送的命令的真实性和完整性),基于 RSA 或 ECDSA 的数字签名将是可靠的解决方案。你是否决定像 TLS 那样使用证书,还是自己管理原始公钥,取决于你的需求和可能性。
此外,如果你的 CAN 总线流量包含机密消息,并且完整性也是你的需求之一,AES-GCM 或 ChaCha20-Poly1305 可能是合适的候选算法。如果你的场景允许管理和分发预共享密钥,你可以避免昂贵的密钥交换算法。如果不行,那么像 TLS 1.3 使用的 DHE 或 ECDHE 可能是你选择的算法。
听起来这似乎有些肤浅,某种程度上确实如此,因为在实现特定方案的过程中,你需要澄清数百个细节。然而,这些信息为你指明了前进的方向。
安全通信中的冗余
如果你已经读到这里,你已经了解了许多关于如何通过使用 TLS 及其神奇的加密特性来保护消息的机密性和完整性,以及通信伙伴的真实性。然而,在本章开头,我提到过通信要求所有保护目标,包括可用性。但明确来说:TLS 和一般的加密技术无法保护可用性。
当然,在某些应用场景中,传输数据的可用性至关重要——例如,在技术安全措施保护人类免受事故和伤害的领域,也包括那些系统中,停机时间会导致显著的财务损失,如生产或运输领域。在这些应用中,如果消息丢失,设备或整个系统的正常功能就会受到威胁。这些威胁必须通过逻辑或物理冗余来处理。
从逻辑层面处理这个问题意味着多次发送消息或添加由错误检测代码(如 CRC 校验和)生成的冗余数据,或像汉明码这样的纠错算法。这对于在嘈杂或不可靠的信道上传输数据,以及处理由电磁干扰或宇宙辐射等引起的干扰事件非常有用。然而,这些措施并不能提供足够的保护,以防止故意破坏和持续中断物理通信线路。
在这种情况下,唯一能保持韧性的方法就是实施多个物理通信通道。一个常见的例子是环形拓扑,它在许多工业基础设施中用于将设备连接成一个环状结构。然而,这种配置要求设备必须具有两个网络接口,左侧和右侧,并且消息必须始终双向发送,并能在两个不同的接口上接收。
这需要在每个设备上进行工程和组件工作,整个系统的安装成本也变得更高。此外,通信延迟取决于环中的设备数量,并且带宽需要在所有网络参与者之间共享。然而,这种物理冗余对于损坏的消息、断裂或被切断的电缆,甚至在系统运行期间更换设备,都是非常坚固的。国际标准 IEC 62439 描述了实现工业网络高可用性的几种方法,例如,基于环形或网状拓扑结构。
总结
物联网及其工业对应物如果没有安全的通信,是无法实现的。现代通信通道需要满足从保密性、完整性、真实性,甚至可用性等各种保护目标。除了必要的密码学能力,设备还需要支持如 TLS 1.3 等先进的协议,以实现高安全级别。在一些应用场景中,当消息丢失至关重要且需要冗余物理通信介质时,设备甚至需要提供多个通信接口。
本章的案例研究展示了一个基于 TLS 通信的 MQTT 代理实现示例,并给出了必要的配置参数。它表明,对最终结果进行彻底的安全分析有助于发现服务中的配置错误和弱加密套件。
虽然 TLS 是最常见和最流行的安全协议,但还有许多其他协议是为特定应用设计的。例如,Internet 协议安全(IPsec)可用于在 OSI 第 3 层建立虚拟专用网络(VPN),而 SSH 协议则使管理员能够远程访问设备。在 OSI 第 2 层,时间敏感网络(TSN)的概念负责在共享物理介质上进行通信通道的分隔,而 IEEE 802.1AE(也称为MACsec)则致力于保护通信安全。
即使你没有找到完全符合需求的协议,现代安全协议的融合也绝对可以为解决你的具体挑战提供灵感。
第三部分:高级设备安全概念
许多经理和开发人员梦想着一劳永逸的安全解决方案。他们认为,一个安全的设备仅仅通过启用操作系统内核中的保护功能,或者仅仅集成一个安全组件到产品的 PCB 设计中就能实现。显然,这是不切实际的想法。设计安全设备需要在从开发到设备生产再到日常运营的各个环节中保持专注和关注。
本书的第三部分涵盖了一些可能对设备安全产生重大影响的概念,比如系统完整性、可靠的更新流程,以及在遭受攻击时的鲁棒性。此外,智能访问控制管理和系统监控可以显著减少漏洞的影响。然而,所有这些只有在全面考虑和实施的情况下才能成功。
第八章:安全启动与系统完整性

在嵌入式系统的启动过程中,基本硬件的初始化以及操作系统的启动都在进行。这些步骤中的许多涉及存储在闪存中的固件,因为这样可以让设备工程师进行更新。然而,这种可替换性是有代价的:攻击者可以篡改这些数据以谋取自己的利益。
在本章中,我将解释启动过程的复杂性和各种保护概念。在介绍经典的安全启动链概念后,我将讨论实际问题,如安全启动过程对开发和生产过程的影响。像往常一样,理论和实践并不完全相同,因此我将包括一个关于在 STM32MP157F 平台上实现安全启动的案例研究。
基于启动过程的完整性,您可能会想知道设备的完整性是否可以进一步加强,因此本章还描述了如何为文件系统等实现完整性保护。最后,我将介绍一种低成本的固件完整性解决方案,适用于不依赖外部闪存和复杂启动过程的微控制器系统。
系统启动复杂性
许多现代嵌入式系统的微芯片包含多种子模块,因此被称为系统芯片(SoC)。除了多个 CPU 核心的可用性外,GPU、实时核心以及类似的支持协处理器也增加了系统的复杂性。此外,包含 FPGA 的 SoC 也越来越受欢迎,并在方程中加入了比特流处理。
此外,一些 SoC 提供受信执行环境(TEE),用于将关键软件的执行从普通固件中逻辑或物理地分离—在一个专用处理单元上。在基于 ARM 的 SoC 中,您会遇到ARM TrustZone和ARM Trusted Firmware这两个术语,它们代表 ARM 的 TEE。这样的环境初始化通常发生在启动过程中。而且,尽管这些特性旨在提高安全性,但不可否认的是,这些技术也导致了更大的复杂性——可理解性和安全性的天然敌人。
SoC 设备通常还需要在 PCB 上添加其他组件,例如,易失性的双倍数据速率(DDR)内存和非易失性存储器,如嵌入式多媒体卡(eMMC)或类似的闪存。必要的片上控制器及其参数的初始化是现代启动过程中的关键部分。图 8-1 概述了 SoC 启动过程中涉及的典型软件和硬件组件。

图 8-1:典型 SoC 启动过程的组件
开机后,启动程序由硬件启动,通过一个启动只读存储器(ROM)➊ 初始化内部结构并加载一个极简的第一阶段引导加载程序(FSBL),例如 U-Boot SPL,该程序被复制到内部 SRAM 内存中➋。这段软件初始化外部 DDR 内存,并将一个完整的第二阶段引导加载程序(SSBL)放置在其中➌。SSBL 能够提供多种便捷功能,如引导介质选择、调试、控制台访问等。随后,操作系统内核(在嵌入式系统中通常是 Linux)被启动 ➍。此时,启动过程“正式”结束,设备进入其运行状态。
图 8-2 显示了按时间顺序启动所需的步骤。

图 8-2:典型启动过程的步骤
这里有一个基本属性需要强调:启动过程中后续的各个阶段始终“信任”其前置阶段。这意味着,例如,在没有外部监控措施的情况下,操作系统无法辨别恶意引导加载程序和原始引导加载程序的区别;它只依赖于每个配置设置及传递给它的参数。因此,最佳的启动过程保护必须从一开始就着手:在硬件层面。
启动保护概念
从客户或设备用户的角度来看,完整性通常是设备固件和软件的期望保护目标。一种相关的风险情景是,网络犯罪分子试图将恶意软件永久植入固件中,以便在系统重启后依然能够存活——例如,启用持久的后门访问。此外,如果在启动时运行验证程序,低级配置参数和操作系统设置的篡改也将变得不可能。即使是情报部门所报道的在交付过程中修改设备软件的攻击,如果实施了可靠的完整性保护,也会导致产品无法正常工作。
IEC 62443(第 4-2 部分)的作者似乎也考虑到了这些情景。安全级别(SL)1 或更高的嵌入式设备要求(EDR)3.14 旨在确保启动过程的完整性。它要求设备必须能够在执行固件、软件和配置数据之前,对其启动和运行过程中的固件、软件和配置数据进行完整性验证。从 SL 2 开始,标准甚至要求对所有可更换的引导过程部分进行真实性验证。
设备制造商通常一致认同固件和软件的完整性和真实性的重要性,因为这还可以防止攻击者安装自定义软件进行逆向工程。然而,他们还需要考虑另一个风险:知识产权的丧失。即使在早期启动阶段,这些技术诀窍也可能以软件的形式存在,比如优化算法、专有协议和机密信息。因此,在某些情况下,供应商希望加密启动过程中的所有固件、软件和配置数据,以保护其机密性。
这个过程中第三方利益相关者是芯片制造行业,它提供了许多由相关营销术语宣传的保护措施。以下列表提供了典型关键词的概述,但当然,它可能无法涵盖所有未来的营销创意:
安全启动 这个通用术语是“经典”且最流行的术语。它通常指的是在整个启动过程中逐步验证软件组件的完整性和真实性。从加密的角度来看,验证是基于数字签名和 RSA 或 ECDSA 等非对称算法。当签名验证失败时,启动过程会停止。其详细信息在第 145 页的《经典安全启动链》中解释。
验证启动 与安全启动类似,这个术语强调启动链中软件部分的加密验证。英特尔将这个术语作为其 Boot Guard 技术的一部分,谷歌则在 Android 和 Chrome OS 中使用该术语。
高保障启动 NXP 使用这个标签来突出 i.MX 设备的启动过程保护。除了其他保护措施外,这种方法还使用数字签名来验证完整性和真实性,正如安全启动所描述的那样。
认证启动 这个术语可能看起来像是指固件和其他软件组件的认证。然而,受信计算组(TCG)使用这个术语来描述一种启动过程,允许报告“平台启动方式的准确记录。”这意味着在启动过程中,软件的各个部分被哈希并写入 TPM。如果要执行经过篡改的软件,启动过程不会被中断,但之后的状态,无论是原始的还是已更改的,可以报告给外部方,或由 TPM 用于授予或拒绝访问存储的机密信息。
测量启动 微软在基于 TPM 的启动保护中使用这个术语,其中软件在启动过程中被测量或哈希。它类似于 TCG 的认证启动。
受信启动 这个术语有时与 TPM 保护的启动过程密切相关。然而,微软也用它来描述 Windows 内核和操作系统组件的验证。
加密启动与所有其他列出的启动保护形式不同,这一概念旨在实现保密性。在这种情况下,固件和相关软件是以加密的方式存储的,通常基于对称加密算法。解密在启动过程中实时发生,通常由硬件支持。
警告
当涉及到启动过程保护的营销术语时,要小心。它们可能会误导人,并且有时只实现了你所期望的部分内容!
经典安全启动链
对于嵌入式设备,最重要的启动过程保护变种是经典的安全启动链,其中每个参与启动过程的组件在交给下一步执行之前都会验证下一个组件。因此,启动过程中的所有软件部分的完整性和真实性可以得到保障,这是许多利益相关者所期望的要求。
图 8-3 显示了从硬件上电到操作系统运行时的线性安全启动过程。

图 8-3:经典安全启动过程的步骤
与图 8-2 相比,请注意每个阶段都有一组额外的密钥。这些是基于非对称加密(如 RSA 或 ECDSA)的公钥,这意味着它们不包含任何秘密,但对于验证随后的启动阶段的签名是必要的。例如,DDR 启动加载程序必须携带与下一启动步骤中执行的操作系统内核签名相关的公钥。
虽然这些密钥不要求保持机密,但它们有一个非常重要的保护目标需要实现:完整性。原因很简单。如果攻击者能够替换公钥,那么可以存储一个自生成的公钥。通过更改验证密钥,就可以正确验证伪造的、自生成的、被篡改的软件签名,这会破坏安全启动链。
图 8-4 显示了涉及的公钥是如何存储在 SoC 内部的各个位置的。最显而易见的是,其中一些通常存储在闪存中,这很容易被攻击者篡改。

图 8-4:参与安全启动过程的 SoC 组件
处理这个问题的“窍门”是,安全启动链的完整性可以简化为第一个验证密钥的完整性。如果这个初始密钥存储在闪存中,那么只有当攻击者无法篡改此内存内容时,安全启动过程的保障才能成立。
支持安全启动的微芯片通常提供 OTP 存储器,将第一个公钥或其哈希值物理地烧录到设备中。然后,在启动 ROM 执行过程中 ➊ 使用该密钥来验证 FSBL。用于 SSBL 验证的密钥包含在 FSBL 中 ➋,而 SSBL 则携带公钥来验证操作系统内核 ➌,从而实现整个启动链的完整性目标。通过将进一步的公钥集成到操作系统内核中,验证链甚至可能扩展到内核模块或应用软件 ➍。
注意
请记住,只有从 SoC 中的 ROM 开始的安全启动链,才能提供对启动软件篡改的强大保护,因为所有保护保证都可以追溯到启动链的开始。
实施安全启动的注意事项
不幸的是,安全启动的实现并不像在配置菜单中激活单个选项那样简单,这也可能是为什么嵌入式系统上的安全启动在市场上仍然不常见的原因。然而,以下细节并非为了让开发者退缩,而是作为一份任务清单,帮助你在决定实施安全启动时牢记。
硬件和软件要求
首先,重要的是要理解,完整的安全启动链要求链中的每个部分都支持基于非对称加密的签名验证。当然,这也包括运行所有软件的微控制器。如果在购买芯片时没有要求安全启动,事后集成可能就不再可行。
确保在设备架构讨论中将安全启动作为一项要求。如果核心微芯片不支持安全启动,你将被迫将安全启动功能推迟到下一代设备。
此外,如果软件组件不支持签名验证(例如某些引导程序可能不支持),且你无法自己集成该功能,你将需要开始可能繁琐的寻找和实现适当替代方案的过程。
在启动过程的最后一步,验证操作系统内核作为最终步骤可能已经足够。然而,举例来说,在 Linux 中,你可能还希望验证内核模块的完整性和真实性,或者甚至将验证过程扩展到文件系统和应用程序,正如在第 154 页的《启动过程之外的完整性保护》中所讨论的那样。
即使所有软件已经支持数字签名验证,实现安全启动仍然需要进行若干更改,从硬件初始化到引导程序设置,甚至可能涉及操作系统内核配置。
开发过程
在开发方面,必须建立签名生成过程(例如,在 CI 管道中)。根据设备中使用的硬件和软件,可能需要集成特定厂商的工具,例如用于签名和最终镜像生成的工具。如果运气好,使用 openssl 命令行工具就足够了。
警告
镜像签名过程对于安全启动实现至关重要。然而,它涉及到私钥,这些私钥需要强有力的保密保护。如果这些密钥被泄露,设备软件的真实性和完整性将无法得到保证。
在开发由安全启动保护的设备时,一个常见的障碍是如何处理测试镜像和设备。存在多种方法,每种方法在特定情况下都有效。一方面,您可能希望在完全开放的设备上进行早期测试,而不受安全启动限制。然而,这样您将无法发现与安全启动相关的问题。另一方面,您可能希望在测试设备上使用一个或多个测试签名密钥,只认证用于测试的镜像。然而,这样做可能会有风险,可能会不小心将测试设备锁定,导致错误的配置或密钥,将设备变成“电子砖”。
无论如何,在使用不同的密钥对进行测试和生产使用时要小心。如果它们混淆,测试镜像(可能带有额外工具或更少的限制)可能会正确验证,并能在现场设备上运行。
生产和生命周期
另一个与安全启动实现相关的领域是设备的生产。由于配置数据和密钥必须烧录到 OTP 存储器中,因此生产过程必须进行调整,以支持安全启动。对安全启动的生产测试也是一个合理的扩展。想象一下,如果之前所有的步骤都经过仔细和努力地执行,但设备在生产过程中以不正确激活的安全启动流程离开,且这个流程在现场很容易被绕过。那么,最好的情况可能是对所有安全工程师来说,这是一次激励上的灾难,可能更糟。
就像任务本身已经足够复杂一样,安全启动实现也是攻击者和研究人员的一个诱人目标。因此,签名密钥可能会在您的开发基础设施中被泄露,或者硬件或固件中影响启动过程安全性的漏洞可能会被外部方发现。这两种情况都要求必须用更新的固件版本替换固件的一部分,可能还包括重新生成的公钥验证密钥。正如第九章中所描述的,安全更新功能以及一个稳健的密钥和固件管理过程,对于专业地处理这些情况至关重要。
注意
实现安全启动需要开发人员、IT 环境管理员和生产工程师的共同努力。确保设备安全被视为所有相关人员的共同目标。
开源许可证与安全启动
钉住特定软件版本与硬件绑定对设备制造商来说非常有价值,但这场博弈还有另一方参与:自由和开源软件社区。
在 2000 年左右,一家名为 TiVo 的公司开发了一种数字视频录像机(DVR),该设备阻止用户执行修改版软件。TiVo 不仅是安全启动的先驱,还吸引了理查德·斯托尔曼和自由软件基金会(FSF)的关注。该 DVR 设备运行的软件下载自 GNU 通用公共许可证版本 2(GPLv2),就像 Linux 一样,旨在使用户能够运行自己定制的软件。从那时起,TiVo 化这一术语被用来描述限制或禁止设备上执行自定义开源软件的机制。
在这场冲突之后,FSF 开发了 GPLv3,这是一种将 GPLv2 中隐含的声明变为明确的许可证:
本节所传达的相应源代码必须附带安装信息。[... ] 安装信息 [... ] 指的是任何安装和执行修改版所需的方法,[... ] 授权密钥,[... ] 等。
尽管 Linux 内核和流行的嵌入式系统引导程序 U-Boot 是基于 GPLv2 许可证的,但像 GNU 大统一引导程序(GRUB)这样的其他软件是基于 GPLv3 许可证的,这在安全启动场景中可能引发法律冲突。
在将产品推向市场之前,明确开源软件许可证与安全启动保护的对立目标是非常必要的,这能帮助你避免不少麻烦。
这似乎不值得深思,但在某些情况下,开发者会寻求一种既能保护启动过程又能安装修改版软件的解决方案。实现这种妥协的一种方式是在启动过程中实现一个解锁功能。该功能允许停用安全启动验证,从而启用自定义(启动)软件的执行,但同时,设备必须确保所有敏感数据,如解密密钥、身份验证器和专有知识,都会从内存中清除。这种方法已经被多家 Android 手机厂商采用。
案例研究:STM32MP157F 设备的安全启动过程
本节中,我将探讨 STM32MP157F 设备及其相应软件包的具体启动过程保护措施。然而,请记住,这只是一个广泛的概述,并非逐步教程。全面的安全启动实现需要大量的工作和细致的调试。
启动过程
手头的微控制器展示了一个常见的启动过程复杂性,如图 8-5 所示。

图 8-5:STM32MP157F 设备的启动链
在上电后,ROM 代码执行平台的基本初始化,将 FSBL 加载到内部 RAM,并将执行权交给 FSBL。在这种情况下,FSBL 是 ARM 提供的受信固件 A(TF-A)中的引导加载程序阶段 2(BL2)部分。在简单的设置中,这个 BL2 初始化设备的 DDR 内存,将 SSBL 加载到其中,并执行 SSBL。然而,如果需要使用 ARM TrustZone,FSBL 不仅会加载 SSBL,还会加载安全世界的运行时软件,然后跳转到 SSBL。
默认情况下,STM32MP157F 平台使用流行的嵌入式系统引导加载程序 U-Boot 作为 SSBL。U-Boot 具有多种功能,常用于引导嵌入式 Linux 内核。接下来,Linux 接管控制,启动其内核模块、服务和用户空间应用程序,并完成启动过程。
STM32MP157F 设备的另一个特点,也是其他微控制器产品和厂商日益常见的特性,是提供协处理器。在这里,集成了一个额外的 ARM Cortex-M4 微控制器,配有专用 RAM,以支持实时任务的强健执行,如第十章中所讨论的那样。将固件加载到这个附加控制器可能也是启动过程的一部分。这可以通过像 U-Boot 这样的 SSBL 直接发起,也可以在运行中的 Linux 操作系统后加载。
许多开发者会停在这里,按描述使用该平台,并且如果启动过程“正常工作”就会感到满意。然而,直到现在,还没有考虑到任何固件部分被修改的保护。
安全启动从硬件开始
强大的安全启动概念必须从硬件开始。因此,ROM 代码需要提供一个验证程序,用于认证 FSBL 镜像。幸运的是,STM32MP157F 设备具备为此目的而集成的功能。图 8-6 展示了相关的密钥生成、签名和认证过程概述。

图 8-6:STM32MP157F 设备的固件签名和供应过程
手头的芯片利用 ECDSA 算法来实现固件镜像的完整性和真实性。与 RSA 相比,这可以实现更快的签名和更短的密钥,但在启动时的验证可能会稍微花费更多时间。在开发者一方,ECC 密钥对必须通过 STM32MP 密钥生成工具生成,结果会得到一个私钥、一个对应的公钥和公钥的 SHA-256 哈希值。后者非常有用,因为它比存储完整的公钥占用的 OTP 内存少。
为了准备设备进行安全启动,必须将公钥哈希烧录到其熔丝存储器中。这可以通过多种方式实现。你可以使用 ST 提供的 STM32 Cube Programmer 或 U-Boot 的stm32key命令。此外,安全密钥配置(SSP)功能可能对你有帮助,因为它在编程器和 STM32MP157F 设备之间建立了一个受保护的通道,尤其在安全生产环境中非常有用。
注意
在将公钥哈希写入 STM32MP157F 的 OTP 存储器后,该设备可以被锁定。生产过程中这是强制要求的,但在开发阶段应谨慎考虑。
在设备中完成密钥配置后,镜像需要准备好进行认证执行。同样,提供了特定的软件 STM32MP 签名工具,该工具处理预生成的 ECDSA 私钥和 FSBL 镜像的 SHA-256 哈希,以获得该固件部分的相应数字签名。生成的镜像还包含 ECDSA 公钥,并可以放置在设备的非易失性存储器中。
启动时,ROM 代码会验证所提供的 FSBL 镜像的完整性和真实性。首先,通过将提供的公钥的哈希与 OTP 存储器中存储的哈希进行比较,验证公钥的有效性。如果正确,公钥将用于验证存储的签名和所提供镜像的哈希。如果验证成功,则执行 FSBL 负载,并在控制台输出NOTICE: Bootrom authentication succeeded。在其他任何情况下,启动过程将停止。
基于 BL2 TF-A 的安全启动
对于 FSBL,STM32MP157F 设备依赖于 ARM 的 BL2 TF-A,它也提供了安全启动支持。然而,默认情况下该功能是禁用的,必须通过设置TRUSTED_BOARD_BOOT=1构建标志来激活。之后,通过 BL2 加载的二进制文件会根据非对称加密技术验证其完整性和真实性。
BL2 TF-A 需要一个固件镜像包(FIP),该包可以通过fiptool应用程序生成。它包含所有需要加载和执行的二进制文件以及验证这些二进制文件所需的加密数据。这些二进制文件包括例如在 ARM TrustZone 中运行的 SSBL 和 TEE 实现,如 OP-TEE。TEE 被标记为 BL32,而 SSBL 在 TF-A 的分类中被称为 BL33。
再次,第一步是生成密钥对(在本例中是 X.509 证书),这些密钥可用于签署和验证固件部分。为此,ST 提供了cert_create工具。
TF-A 要求所有证书都必须是信任链(CoT)的一部分。默认情况下,存储在 STM32MP157F 的 OTP 存储器中的公钥作为根密钥,因此对应的私钥是cert_create工具的强制输入。
在所有二进制文件和相应的证书最终确定后,可以生成 FIP,并将结果部署到设备上。在启动过程中,BL2 TF-A 将使用 Mbed TLS 进行证书解析,并使用 STM32MP Crypto 库进行签名验证,以便使用硬件哈希模块。
二进制加载和执行的顺序如下。首先,BL2 将 BL32(OP-TEE)加载到内存并验证其签名。随后,BL33(U-Boot)也会进行相同的操作。仅在验证成功后,才开始执行 BL32,BL32 完成后将调用 BL33。
U-Boot 的安全启动功能
在 ROM 代码和 BL2 TF-A 之后,设备安全启动中的第三个实现涉及 U-Boot 的镜像认证过程。2013 年,通过引入基于 RSA 的签名验证,用于扁平化镜像树(FIT)镜像,为 U-Boot 的安全启动支持奠定了基础。
为了激活这些功能,还必须启用几个配置选项——例如,RSA 加密功能(CONFIG_RSA)、FIT 支持(CONFIG_FIT)以及 FIT 镜像中的签名处理(CONFIG_FIT_SIGNATURE)。
再次强调,密钥生成是一个基本步骤。然而,由于 U-Boot 是一个开源项目,密钥生成也可以使用开源工具,如openssl命令行工具。在撰写本文时,U-Boot 支持基于 RSA-2048、RSA-3072、RSA-4096 和 ECDSA(使用 256 位曲线)的数字签名。
列表 8-1 展示了生成典型 2048 位 RSA 密钥和相应 X.509 证书的两个命令。两者都需要在后续的签名和镜像生成过程中使用。
$ openssl genrsa -out keys/dev.key 2048
$ openssl req -batch -new -x509 -key keys/dev.key -out keys/dev.crt
列表 8-1:U-Boot 镜像验证的密钥生成
要生成一个正确签名的 FIT 镜像,必须创建一个图像树源(ITS)文件。列表 8-2 展示了一个示例。
/dts-v1/;
/ {
description = "U-Boot fitImage for stm32mp157f";
#address-cells = <1>;
images {
kernel {
description = "Linux kernel";
➊ data = /incbin/("zImage");
type = "kernel";
arch = "arm";
os = "linux";
compression = "none";
load = <0xC0008000>;
entry = <0xC0008000>;
hash-1 {
algo = "sha256";
};
};
fdt-dk2 {
description = "FDT dk2";
➋ data = /incbin/("stm32mp157f-dk2.dtb");
type = "flat_dt";
arch = "arm";
compression = "none";
hash-1 {
➌ algo = "sha256";
};
};
};
➍ configurations {
default = "dk2";
dk2 {
description = "dk2";
kernel = "kernel";
fdt = "fdt-dk2";
signature-1 {
➎ algo = "sha256,rsa2048";
➏ key-name-hint = "dev";
sign-images = "fdt", "kernel";
};
};
};
};
列表 8-2:FIT 镜像源示例
这个 FIT 镜像源示例假设 Linux 内核作为zImage ➊ 可用,并且其设备树二进制文件(DTB)对于 STM32MP157F-DK2 开发板存储为stm32mp157f-dk2.dtb ➋。两者都通过 SHA-256 ➌ 哈希处理,但 U-Boot 还支持 SHA-384 和 SHA-512。
请注意,内核和 DTB 仅在 ITS 文件的configurations部分 ➍ 中合并。这有效地防止了混合攻击,这种攻击试图启动并滥用不合适的内核和 DTB 组合。使用由密钥dev.key ➏ 生成的 RSA-2048 签名 ➎ 来验证包含特定内核和特定 DTB 的配置,只允许执行这种明确的组合。
使用mkimage工具处理 ITS 文件后,最后一个固件部分将被签名,并准备好存储到 STM32MP157F 设备的内存卡中。在 U-Boot 的启动过程中,验证成功时,你将在串口控制台输出中看到类似列表 8-3 所示的消息。
## Loading kernel from FIT Image at c2000000 ...
Using 'dk2' configuration
Verifying Hash Integrity ... sha256,rsa2048:dev+ OK
列表 8-3:U-Boot 成功的内核和 DTB 验证
随后,执行经过验证的 Linux 内核就是你辛勤工作后的值得回报。
即使整个验证链到目前为止“有效”,你仍然有两个重要的任务要完成:
进行全面测试 确保每次固件部分的修改都能被检测到,并且启动过程会相应地停止。很可能,由于疏忽,创建的映像的某个小部分未被相应的签名保护,这可能会为篡改提供机会。
检查硬件已知漏洞 启动过程相关的硬件组件的漏洞可能会破坏整个验证链的安全性。例如,CVE-2017-7932 和 CVE-2017-7936 描述了这种硬件问题,在生产后无法修复。
启动过程之外的完整性保护
经典的安全启动链通常在操作系统接管控制的地方终止。然而,设备架构师可能会有充分理由希望将完整性和真实性保护延伸到此阶段之后。
内核模块验证
可加载的 Linux 内核模块提供了一种模块化和动态的方式来扩展内核功能。然而,修改这样的可加载模块为攻击者通过内核权限执行恶意代码打开了大门。因此,在加载过程中验证这些模块的完整性和真实性是很有必要的。
Linux 内核已经支持这个安全功能。在此,基于 RSA 的内核模块签名可以在加载某个内核模块时进行验证,但该功能默认是禁用的。它必须在内核配置的“启用可加载模块支持”部分启用(CONFIG_MODULE_SIG)。
默认情况下,内核模块签名验证以宽容模式运行:没有签名或相应公钥的模块被标记为污染,但仍然会加载。要强制验证有效的模块签名,必须启用CONFIG_MODULE_SIG_FORCE选项。
在模块签名的标准设置下,内核构建过程会自动在编译时使用 OpenSSL 生成签名密钥和相关的 X.509 证书。创建的私钥用于签署已编译的内核模块,之后可能会被丢弃。当然,包含公钥验证的证书必须集成到 Linux 内核中,以便在运行时进行成功的验证。
文件系统完整性
作为 Linux 启动过程的一部分,内核通常会挂载一个或多个文件系统。这些文件系统可能包含应用程序二进制文件、配置数据、受信任的证书和公钥,所有这些都可能被对手篡改。像 ext3 和 ext4 这样的标准文件系统包含处理意外数据损坏的机制,但从安全角度来看并不提供完整性保护。
在考虑文件系统数据的完整性保护时,重要的是要区分保护静态数据(例如,在关机状态下)与保护运行时数据不被篡改。
基于 MAC 的文件系统保护
像 EncFS 和 gocryptfs 这样的堆叠文件系统,在第五章中提到过用于机密性保护的,可以额外提供完整性保护,形式为 HMAC 或认证加密。这些机制主要针对静态数据保护,因为在运行时更改数据是被允许的,当然也是可能的。它们在文件写入时生成加密校验和,并在读取时验证这些校验和。
此外,完整性保护也可以在块设备级别实现。流行的dm-crypt加密目标用于 Linux 设备映射器基础设施,可以使用dm-integrity(CONFIG_DM_INTEGRITY),在写入时生成认证标签,并在读取数据时验证这些标签。同时,dm-crypt支持如 AES-GCM 和 ChaCha20-Poly1305 等认证加密算法。然而,如果仅需要完整性保护,dm-integrity也可以单独使用。这些措施的目标是保护数据免受攻击者修改非易失性内存中的数据。
只读文件系统
如果你担心运行时文件系统的修改,还有一个更简单的解决方案。只读文件系统如 CramFS 和 SquashFS 不实现文件的写入访问,这意味着即使系统被攻破,也没有办法在运行时修改磁盘数据。这些文件系统的另一个非安全优势是它们的压缩存储,减少了对非易失性内存的需求。然而,具有离线访问存储介质的攻击者可以随意替换文件系统。
全面的完整性保护
最后,一个解决方案提供了对离线和运行时攻击的完整性保护:dm-verity模块(CONFIG_DM_VERITY)。该模块源于 Chrome OS 社区,旨在作为安全启动与文件系统完整性之间的直接扩展。此外,Android 4.4 于 2013 年引入了对它的支持,并且在 2016 年开始严格执行,从 Android 7.0 开始。
从技术角度来看,dm-verity使用哈希树,其中每个数据块在给定的块设备中都被哈希化。然后,将一组哈希值继续哈希,直到得到下一级哈希,以此类推,直到剩下一个根哈希值。如果将此根哈希值纳入安全启动验证过程,就可以保证块设备中所有数据的完整性。在运行时,每次访问文件时,哈希树都会被验证,直到根哈希。当然,这导致了一个只读文件系统。这样的卷的初始化过程由veritysetup用户空间工具在文件系统镜像创建后提供支持。
写保护作为一种低成本解决方案
对于一些微控制器,尤其是低成本和低性能的变种,安全启动可能无法实现。然而,这并不能成为缺乏固件保护的有效理由。
这些平台的启动过程通常要简单得多,因为它们使用实时操作系统(RTOS)或甚至仅运行裸机软件。此外,它们的软件和数据通常更小,并且往往至少部分存储在内部闪存中。但即便如此,完整性和真实性的目标对于保护设备免受恶意修改仍然很重要。
在这种情况下,一个简单但强大的功能是激活内部闪存的写保护。固件被写入设备后,内存会随之“锁定”。同时,调试接口(如联合测试行动组(JTAG))应当被禁用。遵循深度防御原则,在此类固件中实现验证功能是有意义的,这些功能检查锁定位和其他反调试措施是否设置正确。这样,即使写保护机制被绕过——例如通过物理攻击——攻击者仍然需要投入大量的逆向工程工作才能完全破坏固件的完整性保护。
当然,这个内部的、完整性保护的软件可能会实现加密签名验证,并成为验证链的起点,进而将内部保护目标转移到存储在外部内存中的数据上。
总结
处理器硬件及其制造商在嵌入式系统安全启动过程的实现中扮演着重要角色。专有架构、异构多核复杂性以及各种市场营销术语共同构成了设备架构师和开发人员在追求受保护启动过程时必须克服的高障碍。
基本原理很简单:一个不可变的硬件组件验证第一个加载的软件组件的完整性和真实性。如果验证成功,执行控制将交给该软件,之后该软件可能会验证下一个软件组件,以此类推。然而,实际上,如本章的案例研究所示,启动过程的每个阶段都不同,需要特定产品的知识,并且通常需要厂商特定的工具。此外,还必须管理一系列加密密钥,设备生产过程也必须为支持安全启动做好准备。此保护措施对整体设备安全性的显著提升是有代价的。
一个健壮的安全启动实现是进一步安全措施的坚实基础,如内核模块验证、文件系统完整性保护和多种运行时完整性措施。它甚至对逆向工程保护和第五章中描述的安全数据存储方法产生积极影响,因为它可以防止攻击者在你的产品上执行自定义代码,从而探索其内部结构。
第九章:安全固件更新

安全更新对消费者、管理员,尤其是制造商来说,都是令人烦恼的。制造商需要不断监控其产品中可能存在的漏洞,并对相关通知作出反应,而用户和管理员则必须及时应用发布的补丁。由于我们可以将设备的安全性视为一个易变的状态,可能明天就会发生变化,因此拥有一个稳固的更新策略是必不可少的。
然而,软件更新处理并非一件简单的事情。只有授权的实体应该能够向设备提供更新,而且这些更新不应破坏设备的功能或将其变成一块昂贵的砖头。此外,安全专家往往不愿意向其认证设备引入补丁,而相应的认证机构也越来越认识到安全更新在安全关键领域中的重要性。
本章介绍了安全更新方法的选择及其背后的原因。一个核心考虑因素是安全地实施更新验证,并确保其在设备上的可靠应用。最后,本章通过一个基于流行的 SWUpdate 框架和 Yocto 工具链的固件更新实践案例进行总结。
更新的不可避免性
在一些软件和产品开发社区中,永恒的 beta阶段是常见的:产品永远不会脱离 beta 状态,新的功能不断添加,即使软件或设备已经投入使用,并由客户在实际环境中使用。这一概念有时也被称为香蕉原则,因为这些水果在还未成熟时就被采摘,并在运送到客户的途中甚至购买后继续成熟。在这种情况下,显而易见,交付的产品并没有完全完成,且需要多个软件更新才能开发出其全部潜力。因此,一个安全的更新方法至关重要。
如果我们看一下工业系统和关键基础设施,情况就截然不同。虽然这些系统通常具有较长的使用寿命,但其制造商和运营商在过去可能没有看到补丁管理的必要性。然而,随着这一领域的连接性和数字化的增加,以及工业产品中漏洞的不断发现,迫使供应商和用户采取行动并为安全更新过程做好准备。这种情况尤其具有挑战性,因为工业组件的制造商对安全更新的支持和操作补丁管理过程通常需要持续数十年。
即使你在一个具有强烈安全重点的行业工作,并且你的开发过程产生了高度安全且稳健的产品,你也无法保证你的软件、固件和硬件组件没有漏洞。而且,你无法预知是否会有新的攻击方法——这些方法在产品开发时甚至未曾被发明——会对你的设备造成安全问题,并可能要求更换加密算法或进一步增强安全功能。简而言之:没有任何产品是完美的。你需要让它具备可更新性。
一些显著的案例强调了嵌入式和物联网设备需要支持安全更新。2019 年的Urgent/11和 2020 年的Ripple20漏洞集合显示,像 TCP/IP 协议栈和操作系统这样的基础软件组件可能存在严重的弱点。数百万,甚至数十亿设备受到影响,许多设备被认为无法修补,因为它们不提供必要的修补手段。最终,这种无法修复的状况迫使客户更换这些设备,或者让他们继续使用不安全的系统。在这种规模下,它甚至可能对整个社会构成风险,因为恶意行为者知道如何将这些物联网产品引入他们的僵尸网络。归根结底,2020 年代销售的每一台数字设备都应该具备一个安全的更新机制。
注意
我并不是唯一一个强调安全更新策略必要性的人。工业网络安全标准 IEC 62443、联合国(UN)针对汽车行业的法规 156 以及美国 FD&C 法案中针对医疗设备的网络安全部分也有类似的共识,仅举几例。
安全要求
与任何带有安全字眼的概念一样,显而易见的问题是,在这个特定上下文中,它关联了哪些(保护)目标。以下部分描述了你在安全固件更新中必须考虑的要求。
真实性
固件更新必须能够通过加密方式证明其真实性,这确保了更新来自给定设备的原始制造商。这可以防止恶意制作的更新安装,并且应该成为所有安全更新程序的强制要求。这个目标通常是通过数字签名更新包来实现的。
保密性
固件镜像通常被人们用来逆向工程设备,以枚举软件库、识别弱点或分析专有应用程序中的知识产权和机密信息。可以通过加密整个更新内容或其部分内容来实现保密性保护。
然而,请确保你理解存在风险,因为相应的解密密钥需要存储在设备上,攻击者可能能够从设备中提取该密钥,或者在设备解密后的固件更新中提取明文。
安全分发渠道
前两个要求,即真实性和保密性,对更新文件本身提出了要求。然而,分发渠道本身也值得保护(例如,使用 TLS)。
设备与更新服务器之间的相互认证和加密通信,甚至可以替代更新文件的保密保护。
回滚选项
这点比较棘手。将设备的软件版本回滚到较早状态有时非常有用——例如,如果更新引入了之前不存在的严重问题。另一方面,攻击者可能利用此功能将设备的软件回滚到包含已知漏洞的版本,而这些漏洞已经被后续的补丁修复。在这种情况下,尽管制造商提供了安全更新,设备仍可能处于易受攻击的状态,攻击者可以利用这些漏洞进行攻击。
如果你决定实施回滚保护,那么你需要相应的硬件支持,例如在主 CPU 中实现一个单调、不易丢失的版本计数器。
版本分发监控
监控现场安全更新的采用情况可能非常有价值,因为这可以帮助你了解设备和客户的整体安全态势和威胁情况。你可以通过在每次成功安装新固件版本后,向每个设备发送确认通知来实施这一监控。
更新的分发与部署
关于固件更新的一个核心问题是:“更新文件如何找到它所在的设备?”对此问题的回答对设备的可用性、流程和补丁管理的反应速度有着重要影响。
本地更新 vs. 远程更新
自 1990 年代以来,实施更新机制的设备通常提供某种接口来上传或存储以前从制造商网站下载的固件文件。更新文件甚至可能被限制只能通过本地接口加载,如通过 USB 设备。虽然本地更新可能是一项安全特性,但这种方法对于物联网场景的扩展性较差。数百万设备需要数百万具有安全意识的客户以及数百万次手动更新安装,这几乎是不可行的,并且导致实际应用的更新很少。
汽车行业是一个关于固件更新的有趣例子。多年来,如果汽车存在严重的软件漏洞或缺陷,通常会发布召回通知。车主需要将车辆送到修理厂,由机械师安装汽车制造商提供的软件更新,这些更新通常是由特定控制单元的供应商提供的。如今,现代汽车可以通过连接到移动网络来接收软件更新。这种方法被称为空中下载(OTA)更新。
OTA 更新不仅限于汽车。它可以应用于各种物联网设备,尽管这个术语暗示更新是通过无线通道进行的,但通过有线网络传输更新显然并不被禁止。这个概念的主要优势在于设备能够连接到更新管理后台,通常由设备制造商运营,提供关于新更新的信息。这种方法提供了不错的可扩展性,因为更新发布过程可以由制造商自动化并进行计划。然而,这样的系统将后端服务器和监听更新的设备暴露给远程攻击者和基于网络的攻击。
注意
在一些工业场景中,手动本地更新程序仍然是默认的。我听说有个人亲自访问数百台工业机器人,插入 USB 闪存驱动器,然后等待更新完成后才继续他的旅程,可能一次更新就需要几周时间。疯狂!
拉式与推式策略
毋庸置疑,在后端设备与更新服务器之间建立直接连接是专业更新管理的合理解决方案,但仍有一个问题:谁来控制更新过程?
通常,设备采用拉式策略进行更新管理:设备上的客户端应用程序定期检查是否有新更新,并下载新发布的镜像文件。随后,根据所有者的配置,设备可能会自动安装更新(例如,在预定的维护窗口内),或提示用户或管理员批准安装。更新成功后,新的软件状态可能会报告给制造商的服务器。在这种情况下,操作员控制何时以及安装哪些更新,这通常是商业场景下首选的解决方案,但他们也需要负责定期的更新安排。
另外,制造商可能会选择推式策略,这将使他们在更新过程中拥有更多的控制权。他们甚至可能强制设备进入更新模式。如果制造商负责及时更新设备,甚至可能有法律义务,这种方法是合理的。此外,如果更新被自动强制执行,特定客户群体(如个人用户)的安全性可以显著提升,因为他们无需自己组织更新过程。另一方面,一些客户故意选择使用旧的固件版本,如果他们拒绝让设备访问更新服务器,制造商也无能为力。然而,如果这类情况是你所面临的挑战,专注于软件状态监控就显得有意义,当设备连接到在线服务时,若固件过旧,则拒绝其访问,确保大量设备保持在安全状态。
在实际操作中,混合拉取和推送策略是可能的。你甚至可以通过为设备提供相应的配置选项,将决定权交给客户。这使得操作人员能够将你的设备完美地集成到他们特定的资产和补丁管理流程中。
更新粒度和格式
软件更新和固件更新这两个术语通常可以互换使用,并适用于各种更新场景,从重写设备内存的所有内容到更改单个文件中的一些配置参数。因此,对于你的特定设备,明确哪些固件部分是可替换的,以及你希望以何种格式分发这些数据,是绝对必要的。
固件部分
在 PC 环境中,固件是指编程到主板、安装的子板以及其他外围设备上的板载非易失性存储器中的软件。操作系统、软件应用程序和用户数据,通常存储在硬盘上,不被视为固件。然而,在嵌入式系统环境中,几乎一切都可以被视为固件:
引导加载程序 现代嵌入式设备通常至少包含一个引导加载程序,通常不止一个。
协处理器固件 最近的 SoC(系统级芯片)包括在一个封装内的异构处理器集合。这些协处理器中的一些可能需要自己的固件。
控制器固件 在 PCB(印刷电路板)层面,一个嵌入式系统可能有多个微控制器,每个微控制器执行专用的固件。
FPGA 比特流 作为 SoC 的一部分或作为独立组件,FPGA 被集成到多种嵌入式设备中。它们的配置,称为比特流,可能直接从闪存读取,或由引导加载程序或操作系统应用程序加载。
裸机软件 在没有操作系统的设备上,裸机软件是主要的应用程序。
操作系统内核 如果你的设备带有操作系统或实时操作系统(RTOS),则相应的操作系统内核是中央软件组件。
设备树 特定嵌入式系统的硬件组件及其参数通常在设备树文件中描述,该文件由操作系统内核加载。
根文件系统 所有其他文件系统和覆盖层都挂载在其上的基本文件系统包含对操作系统正确运行至关重要的数据。
应用软件 应用软件可能是根文件系统的一部分,但也可能位于一个或多个制造商特定的分区中。
列表中的一些固件部分可能共享一个公共的非易失性存储器;其他部分可能配有自己的独立存储器组件。在开发安全更新方案时,确保你了解所有相关的固件组件。然而,不要忘记,你的设备可能包含一些独特的数据,这些数据不能被软件更新所影响:
唯一的加密密钥 与设备唯一身份相关的数据,可能在生产过程中提供,对于可信的设备认证至关重要。此外,像在首次启动时生成的 SSH 密钥这样的密钥也应该在软件更新后保持不变。
用户依赖的系统数据 客户依赖于额外的用户账户及其相应的凭证,以及自定义设备配置文件。
运行时数据分区 您的设备可能会收集并存储特定用户的运行时数据,比如用于数据分析应用的传感器值历史记录,也可能会记录与维护和修理相关的数据。
更新格式
如前所述,需要更新的软件组件的粒度差异很大,这并不会直接指示更新分发的具体格式。在决定之前,应该考虑几个要求:
全面覆盖 所选择的格式应能够更新尽可能多的设备软件组件。
效率 由于固件的大小不断增加,应该能够将固件更新限制为实际更新的部分。
原子性 更新的安装应为不可分割的操作,只允许两种最终状态:成功更新的固件,或在失败时回到更新开始前的原始固件。
以下列表提供了基于先前提出的要求的可能更新格式解决方案:
文件 一种简单的更新格式是为设备提供一组更新的文件,这些文件可以写入文件系统中对应的路径。虽然这种方式可以为设备文件系统中所有可用的组件提供更新,并且可以创建小巧高效的更新包,但原子性比较复杂。每个创建、写入或删除的文件都需要进行各自的操作,这些操作可能成功,也可能失败。如果出现错误,固件可能会处于一个未知的状态。
容器 容器化应用在更大型的嵌入式系统中变得越来越流行。替换整个容器镜像可能对其更新管理有效,但仅依赖容器更新会忽略一些重要的软件组件,如运行容器管理的主机操作系统。
图像 更新整个分区镜像可以使嵌入式系统中的许多软件组件进行更新,并且在原子性方面具有优势。虽然这种方法可能导致比其他格式更大的固件更新,但一些实现也支持压缩更新文件或差分更新,以解决这个问题。
注意
无法通过文件系统访问的固件部分,例如必须基于专有协议进行更新的 IC,需要定制的处理。请考虑利弊,并在针对设备的更新能力方面做出有意识的决策。
包管理器的问题
你可能会想,为什么基于 Linux 的嵌入式设备大多数情况下不依赖于像 apt-get 或 opkg 这样的成熟包管理器,这些包管理器在桌面和服务器系统中广泛使用。原因在于基于包更新的系统的测试复杂性。此类方法需要管理系统中所有可能包之间的依赖关系,并且会增加需要测试的可能软件配置数量。
此外,由于嵌入式设备通常在关键应用或行业中运行,因此必须进行全面的测试。因此,许多制造商避免使用包管理器,转而选择基于镜像的原子更新方法,限制软件组件之间的交互,只测试特定版本的软件发布中的组件。
如果你打算走这条路,你可能需要一个提供自动化和可靠测试服务的合作伙伴,来处理所有提到的问题,例如 Canonical 为其 Ubuntu Core OS 提供的服务。
设备分区策略
只有当你的设备的内存分区支持时,才能实现可靠的更新过程。根据可用内存和风险承受能力,可以使用不同的方法来设计系统的分区布局,以支持更新过程。
更新/恢复分区
你可以通过引入一个额外的恢复分区来增强对固件更新失败的容错性。该分区包含下载和更新主系统分区的工具,包括操作系统内核和相应的根文件系统,如图 9-1 所示。

图 9-1:用于执行系统更新的恢复分区
这种方法的优点是额外分区的内存占用较低,这对于许多设备来说应该是可行的。然而,缺点是设备必须重启才能进入更新或恢复模式。如果更新过程失败,主分区会被损坏并无法再启动,此时会再次启动恢复分区,并可以启动一个全新的更新过程。
A/B 系统方法
对于提供大量非易失性存储器的设备,A/B 系统方法可能是一个有趣的候选方案。图 9-2 展示了基本的分区布局和更新过程。

图 9-2:两个相同的系统镜像副本——A 和 B
在这种布局中,系统分区有两个副本,A 和 B,都至少包含操作系统内核和根文件系统。在启动时,标记为“可启动”的分区(在图 9-2 中为 A 分区)用于加载操作系统和基础文件系统。此过程包括一个更新客户端,能够接收固件更新,验证它们,并将其写入当前未使用的分区 ➊(在图 9-2 中为 B 分区)。随后,引导加载程序会被配置为将启动分区从 A 切换到 B(或反之)➋,然后重新启动到新的固件版本。
如果更新失败或分区损坏,旧版本仍然保持原样并可以重新启动。与使用恢复分区相比,一个显著的优点是,设备的标准操作在更新下载和安装过程中不会中断。此外,新下载的固件可以直接存储在非活动分区中,而不需要额外的存储位置来缓存更新。在 Android 设备上,这个概念被称为无缝系统更新,并且越来越多的手机厂商开始实现这一功能。
对于更高关键性的设备,可以将这两种方法结合使用,如图 9-3 所示。

图 9-3:带有额外恢复分区的 A/B 架构
该设计旨在防范系统分区 A 和 B 因各种原因而都遭到损坏的情况。对于这种架构,最好将恢复分区存储在与系统分区不同的物理内存中。即使恢复分区无法成功重新安装系统分区,它仍然可以报告系统故障并执行诊断。
关于更新引导加载程序的说明
引导加载程序广泛应用于许多嵌入式系统。它们处理基本的系统初始化,选择启动介质,并随后加载操作系统内核。在许多情况下,这些二进制文件被视为“不可更改”的固件部分。
然而,如今像 U-Boot 和 GRUB 这样的引导加载程序已经成为功能复杂的软件组件,具有多种特性和能力。因此,引导加载程序可能会出现错误,甚至出现需要更新的安全漏洞。
从安全角度来看,你可能还需要更换用于安全启动过程的公钥或过时的加密算法,正如第八章中所描述的那样。在功能方面,你可能会遇到需要更新硬件初始化设置、内核启动参数或引导配置的情况。所有这些原因都使得更新引导加载程序及其配置数据成为一个有效的考虑事项。
然而,在大多数系统中,每个引导加载程序只有一个副本可用,这意味着更新它存在使设备发生故障的风险——例如,在引导加载程序更新期间发生断电或其他故障时。只有少数几个 SoC 和微控制器提供对额外引导加载程序实例的支持,这些实例可以在主引导加载程序失败时运行。因此,更新引导加载程序始终是一个关键且风险较高的过程,可能导致需要对设备进行物理访问以进行修复的情况。
一种折衷的解决方案可能是多阶段引导加载程序方法,将功能分成两部分。第一阶段被认为是不可变的,具有最小的功能,但提供对下一阶段中多个引导加载程序副本的支持,后者包含完整的引导加载程序。在这种系统中,早期阶段的安全问题仍然是一个问题,需要物理访问或冒险更新过程,但第二阶段可以冗余存储,从而允许低风险的更新。然而,这种方法不是标准的,并且需要进一步的开发工作和定制。
开发、后端和设备之间的相互作用
在澄清了所有影响更新策略的细节后,是时候讨论可靠且安全的固件更新所需的操作流程了。如第一章所述,制造商有责任监控在现场被利用的设备漏洞,并认真对待漏洞报告。
假设你已经解决了这个问题,并且你的开发团队能够在短时间内提供修复。仍然有一个操作性问题:是否应将安全更新和功能增强与固件更新合并,还是应该将它们分开提供?
一些客户需要在其指定的基础设施中对你的设备进行全面测试,因此避免升级设备以加入可能需要重新测试的新功能。然而,他们可能对有助于提高系统稳健性和安全性的安全修复感兴趣。在这种情况下,建议将功能性更新和保护性更新分开。
此外,假设一个新的固件版本导致了问题,客户必须降级到之前的版本。如果安全更新包含在此更新文件中,客户将不得不忍受设备中已知的漏洞,至少在功能性问题得到修复之前。然而,认真对待这种分离会导致更多的软件配置和制造商方面更多的测试。
无论固件更新的内容如何,都必须由开发团队和相应的构建流水线提供和打包。如果需要,固件必须加密,通常使用像 AES 这样的对称加密方案。为了确保真实性和完整性保护,最终的固件镜像必须使用适当的算法(如 RSA 或 ECDSA)进行数字签名。
这两个任务包含两个必须匹配的组件。在制造商一方,通过构建系统生成的固件工件必须加密并签名,以生成实际的固件更新文件进行分发。在设备本身,需要执行签名验证和解密操作。
有多个基于镜像的更新系统可以为你执行这些任务:Mender、SWUpdate 和 RAUC。Mender 提供完整的基础设施,包括设备客户端软件和后端服务器。SWUpdate 和 RAUC 生成并应用固件更新文件,但更新分发和监控通常由 Eclipse hawkBit 后端框架执行。其他候选方案包括 OSTree 和 swupd,它们采用类似 Git 的基于修订的固件更新方法。
案例研究:使用 SWUpdate 进行安全固件更新
本案例研究提供了将安全固件更新架构付诸实践所需的步骤示例。它基于 ST 为 STM32MP157F-DK2 开发板提供的 Yocto 工具链。
我选择了 SWUpdate 作为本实现的核心软件组件。期望的结果是一个基于数字签名验证更新真实性,并确保通过 A/B 系统方法保证可靠更新过程的更新流程。
SD 卡布局定制
Yocto 构建系统编译并打包我设备固件镜像的所有相关软件,并以 .ext4 和 .tar.gz 等格式提供。这对于进一步处理很有用,例如生成 SD 卡镜像和创建更新文件,但我们先一步一步来。
来自 ST 的 STM32 Cube Programmer 工具以及便捷的 shell 脚本 create_sdcard_from_flashlayout.sh 使用制表符分隔值(TSV)格式的闪存内存布局文件。该 TSV 文件包含多个内容,其中包括分区列表、偏移量以及填充这些分区所需的文件。STM32MP157FDK2 开发板的默认闪存布局的选定列如 列表 9-1 所示。
Name Offset Binary
fsbl-boot 0x0 arm-trusted-firmware/tf-a-stm32mp157f-dk2-usb.stm32
fip-boot 0x0 fip/fip-stm32mp157f-dk2-optee.bin
fsbl1 0x00004400 arm-trusted-firmware/tf-a-stm32mp157f-dk2-sdcard.stm32
fsbl2 0x00044400 arm-trusted-firmware/tf-a-stm32mp157f-dk2-sdcard.stm32
metadata1 0x00084400 arm-trusted-firmware/metadata.bin
metadata2 0x000C4400 arm-trusted-firmware/metadata.bin
fip-a 0x00104400 fip/fip-stm32mp157f-dk2-optee.bin
fip-b 0x00504400 none
u-boot-env 0x00904400 none
bootfs 0x00984400 st-image-bootfs-openstlinux-eglfs-stm32mp1.ext4
vendorfs 0x04984400 st-image-vendorfs-openstlinux-eglfs-stm32mp1.ext4
rootfs 0x05984400 st-image-core-openstlinux-eglfs-stm32mp1.ext4
列表 9-1:STM32MP157F-DK2 开发板的默认闪存布局
TSV 文件包含了许多与引导加载程序和受信固件工件相关的行,这些内容在此时并不重要。然而,最后三行揭示了两个重要事实。首先,一个专用的 bootfs 分区包含 U-Boot 配置文件、设备树二进制文件和作为 uImage 的 Linux 内核。其次,rootfs 分区是一个非常适合复制以支持 A/B 系统方法的候选者。列表 9-2 展示了我对 TSV 文件所做的更改。
Name Offset Binary
...
bootfs 0x00984400 st-image-bootfs-openstlinux-eglfs-stm32mp1.ext4
vendorfs 0x04984400 st-image-vendorfs-openstlinux-eglfs-stm32mp1.ext4
rootfs-a 0x05984400 st-image-core-openstlinux-eglfs-stm32mp1.ext4
rootfs-b 0x35984400 st-image-core-openstlinux-eglfs-stm32mp1.ext4
userfs 0x65984400 st-image-userfs-openstlinux-eglfs-stm32mp1.ext4
列表 9-2:A/B 更新方法的调整分区布局
原始的 rootfs 分区被克隆, resulting in two new partitions: rootfs-a 和 rootfs-b。此外,还添加了一个 userfs 分区,这个分区在 ST 提供的工具链中已经存在,用于存储在根文件系统更新过程中应该“保留”的数据。
对于使用新参数生成 SD 卡镜像,还必须调整 create_sdcard_from_flashlayout.sh 脚本,以正确处理新引入的分区 rootfs-a 和 rootfs-b。SD 卡镜像的大小被设置为 2,048MB,同时两个根分区被配置为各占 768MB,这在闪存布局文件中的偏移量0x30000000已经体现出来。
在镜像创建脚本中的一个重要设置是为分区 rootfs-a 和 rootfs-b 分配单独的 UUID,如列表 9-3 所示。
DEFAULT_ROOTFSA_PARTUUID=e91c4e10-16e6-4c0e-bd0e-77becf4a3582
DEFAULT_ROOTFSB_PARTUUID=997046a6-c6f4-4f41-adb4-9fe614b2a92a
列表 9-3:两个根文件系统副本的独立 UUID
我重用了原始 rootfs 分区的 UUID 用于 rootfs-a,并为 rootfs-b 随机生成了一个新的 UUID。这些 UUID 非常重要,因为它们用于 U-Boot 的 extlinux.conf 配置文件中,位于 bootfs 分区,用于确定 Linux 内核挂载为根文件系统的分区。
之后,案例研究的基本分区架构已经准备好支持 A/B 系统更新。
SWUpdate 的安装和配置
SWUpdate 是一款功能丰富的嵌入式系统固件更新工具,它为 Yocto 提供了相应的 meta-swupdate 层。可以从其 Git 仓库克隆该层,并将其添加到 STM32MP1 Yocto 项目中。
安全特性的添加
第一个任务之一是创建一个补丁,修改 SWUpdate 的配置以启用签名和加密的镜像。列表 9-4 展示了必须显式激活的三行配置。
CONFIG_HASH_VERIFY=y
CONFIG_SIGNED_IMAGES=y
CONFIG_ENCRYPTED_IMAGES=y
列表 9-4:激活 SWUpdate 的重要安全特性
两个选项 CONFIG_HASH_VERIFY 和 CONFIG_SIGNED_IMAGES 为 SWUpdate 增加了验证哈希和数字签名的能力,用于验证软件更新中包含的镜像。CONFIG_ENCRYPTED_IMAGES 选项启用了对 AES 加密镜像的支持,尽管我们目前未实现这一功能,但它可能是未来一个有价值的选项。
密钥生成
正如在第二章中所解释的,数字签名是一种非对称加密原语,允许验证签名数据的完整性和真实性。SWUpdate 可以基于普通的 RSA 密钥或证书生成签名。在这个案例研究中,我选择使用 4,096 位的 RSA 密钥。它们的生成可以分为三个步骤,如清单 9-5 所示。
$ echo "SuperS3cr3t" > passphrase
$ openssl genrsa -aes256 -passout file:passphrase -out swu_signing_key.pem 4096
$ openssl rsa -in swu_signing_key.pem -pubout -passin file:passphrase
-out swu_verification_key.pem
清单 9-5:用于更新签名和验证的 RSA 密钥生成
首先,创建一个密码短语文件,里面应该包含一个强密码。之后,可以生成 RSA 密钥(例如,4,096 位长度)。生成的私钥基于给定的密码短语文件进行加密,并存储为swu_signing_key.pem。请注意,这是在构建过程后,用于签署固件更新的 RSA 私钥。对应的公钥被提取并保存在清单中的第三行,命名为swu_verification_key.pem。这样做是为了将该公钥包含在最终设备固件中,因为它在更新签名验证时是必需的。
软件集合
接下来,SWUpdate 需要知道哪些 Yocto 构建产物应包含在固件更新中。这些信息存储在sw-description文件中,该文件用于更新生成,同时也包含在固件更新包中。
清单 9-6 展示了我为此用例定义的软件集合。
software = {
version = "0.1.0";
➊ hardware-compatibility: ["C02"];
stable = {
➋ rootfs-a: {
images: (
{
➌ filename = "st-image-core-openstlinux-eglfs-stm32mp1.ext4.gz";
➍ compressed = "zlib";
➎ device = "/dev/mmcblk0p10";
➏ sha256 = "$swupdate_get_sha256(st-image-core-...-stm32mp1.ext4.gz)";
});
}
➐ rootfs-b: {
images: (
{
filename = "st-image-core-openstlinux-eglfs-stm32mp1.ext4.gz";
compressed = "zlib";
device = "/dev/mmcblk0p11";
sha256 = "$swupdate_get_sha256(st-image-core-...-stm32mp1.ext4.gz)";
});
}
}
}
清单 9-6:在 sw-description 文件中定义的软件集合
你可能遇到的第一个问题是hardware-compatibility参数 ➊。我将其设置为C02,因为这是我的 STM32MP157F-DK2 开发板的硬件和组装版本。在运行时,这个参数必须与/etc/hwrevision中提供的数据匹配(例如,在我的情况下为stm32mp157f-dk2 C02)。
其次,这个软件集合中表示了rootfs-a ➋和rootfs-b ➐两个分区,尽管在运行时只有一个分区会被用于更新。两者都包含了相同的更新目标文件名 ➌——在这个例子中,是由 Yocto 生成的根文件系统。compressed参数 ➍指示数据是否以压缩形式提供,而sha256 ➏将提供的目标文件的 SHA-256 哈希值集成到sw-description文件中。
这两个映像仅在一个属性上有所不同:由device参数 ➎指示的分区。这决定了 SWUpdate 客户端将更新写入哪里。在本例中,/dev/mmcblk0p10和/dev/mmcblk0p11是之前创建的rootfs-a和rootfs-b分区在 Linux 中的设备名称。如果/dev/mmcblk0p10是活动分区,则更新必须写入/dev/mmcblk0p11,反之亦然。
更新文件生成配方
SWUpdate 的固件更新文件扩展名为.swu。meta-swupdate层提供了一个类,支持基于先前构建并存储在 Yocto 的deploy目录中的工件,通过 Yocto 生成 SWU 文件。清单 9-7 显示了一个 Yocto 配方的相应代码,该配方自动生成有效且经过数字签名的 SWU 文件。
# Local files to be added to the SWU file
SRC_URI = "file://sw-description"
# Images to build before creating the SWU file
IMAGE_DEPENDS = "st-image-core"
# Images to include within the SWU file
SWUPDATE_IMAGES = "st-image-core-openstlinux-eglfs"
# Format of image to include
SWUPDATE_IMAGES_FSTYPES[st-image-core-openstlinux-eglfs] = ".ext4.gz"
# SWU signing parameters
SWUPDATE_SIGNING = "RSA"
SWUPDATE_PRIVATE_KEY = "path-to-signing-key/swu_signing_key.pem"
SWUPDATE_PASSWORD_FILE = "path-to-unlocking-passphrase/passphrase"
清单 9-7:生成 SWU 更新文件的配方代码
配方添加了先前创建的sw-description文件,并声明了对st-image-core镜像的依赖。在 Yocto 的deploy目录中,所需的更新内容(设备的根文件系统)可以在以st-image-core-openstlinux-eglfs开头的文件中找到,同时目标机器会由 SWUpdate 自动添加。
与 SD 卡镜像创建不同,在 SD 卡镜像创建中使用带有.ext4扩展名的文件(如清单 9-2 所示),固件更新应尽可能小,以优化传输时间。因此,用于 SWU 文件的压缩工件采用.ext4.gz格式。最后,为了启用固件更新签名,我将SWUPDATE_SIGNING设置为"RSA"并提供了私有 RSA 签名密钥及其相应密码文件的路径。
运行这个配方确保构建了st-image-core镜像,然后在deploy目录中生成swupdate-swu-gen-openstlinux-eglfs-stm32mp1.swu文件。这个文件实际上是一个进出拷贝(CPIO)归档文件,包含sw-description文件及其签名sw-description.sig文件。此外,压缩的st-image-core-openstlinux-eglfs-stm32mp1.ext4.gz文件也包含在此归档中,里面是根文件系统。
注意
如果你想知道为什么根文件系统没有签名文件,记住 sw-description 文件包含该镜像的哈希值,如果有人篡改过该镜像,这个哈希值会发生变化。描述文件的签名也保护了根文件系统的真实性和完整性。
设备特定定制
现在让我们来看看设备、更新客户端以及为了使 SWUpdate 正常工作所需的定制。
更新守护进程
要安装 SWUpdate 的设备客户端及其 Web 服务器组件,必须将swupdate和swupdate-www添加到 ST 的核心镜像中。对于配置,设备端的第一个考虑是它应该支持哪些更新方法。通常,SWUpdate 提供三种典型的更新方式:
Mongoose 模式 这个守护进程提供了一个简单的 Web 界面,允许通过网络手动进行更新。
Suricatta 模式 结合 Eclipse 的 hawkBit,SWUpdate 支持全面的 OTA 更新设置,可以由中央服务器实例进行管理和控制。
本地安装 如果 SWU 文件本地可用(例如在 USB 闪存驱动器上),可以直接安装更新,无需网络连接。
在这个案例研究中,我选择了 mongoose 守护进程。由于该系统使用 systemd 进行 Linux 服务配置和管理,因此可以创建如 列表 9-8 所示的服务文件 (swupdate.service),并将其安装到 /etc/systemd/system/ 目录中。
[Unit]
Description=SWUpdate daemon
[Service]
Type=simple
ExecStart=/usr/bin/swupdate -w '-r /www -p 8080' -e 'stable,rootfs-b'
[Install]
WantedBy=multi-user.target
列表 9-8:一个基本的服务文件,用于在启动后以 mongoose 模式启动 swupdate
SWUpdate 的二进制文件位于 /usr/bin/swupdate。它可以在系统启动时通过 -w 命令行参数以 mongoose 守护进程模式启动。紧随其后的 -r /www 和 -p 8080 参数告诉它使用位于 /www 的默认 Web 应用,并将其 Web 服务器绑定到 8080 端口。
-e 选项后提供的字符串定义了在更新情况下应该安装的预期软件集合的部分。在默认情况下,如果 rootfs-a 是活动分区,则守护进程应使用 -e 'stable,rootfs-b' 启动,以确保潜在的更新写入 rootfs-b,即非活动分区。
除了通过命令行参数设置配置选项外,您还可以提供与客户端编译时配置匹配的 swupdate.cfg 配置文件路径——例如,/etc/swupdate/。如 列表 9-9 所示,日志参数、密钥路径和更新后 shell 脚本是您可能希望放置在此处的典型设置。
globals :
{
verbose = true;
loglevel = 5;
syslog = true;
postupdatecmd = "/etc/swupdate/postupdate.sh";
public-key-file = "/etc/swupdate/swu_verification_key.pem";
};
列表 9-9:一个 swupdate 的示例配置文件
然而,使用命令行参数还是配置文件,主要取决于个人喜好。
更新后任务
每个设备、其架构和更新策略都不同。因此,像 SWUpdate 这样的通用工具无法自动推断在固件更新写入相应的存储设备或分区之前和之后该做什么。在命令行中,-p 和 -P 参数分别用于定义 更新后 和 更新前 命令的路径。
在我的案例中,只需要一个更新后的例程来准备设备引导新的固件版本。列表 9-10 显示了 postupdate.sh shell 脚本的内容,该脚本在更新安装后执行,正如 swupdate.cfg 中所定义的那样。
#!/bin/sh
➊ if grep -q PARTUUID=e91c4e10-16e6-4c0e-bd0e-77becf4a3582
/boot/mmc0_extlinux/stm32mp157f-dk2_extlinux.conf; then
# Update swupdate service parameters
mount PARTUUID=997046a6-c6f4-4f41-adb4-9fe614b2a92a /mnt
➋ sed -i 's/rootfs-b/rootfs-a/g' /mnt/etc/systemd/system/swupdate.service
umount /mnt
# Update rootfs boot parameter in extlinux.conf
➌ sed -i 's/PARTUUID=e91c4e10-16e6-4c0e-bd0e-77becf4a3582/
PARTUUID=997046a6-c6f4-4f41-adb4-9fe614b2a92a/g'
/boot/mmc0_extlinux/stm32mp157f-dk2_extlinux.conf
else
# Update swupdate service parameters
mount PARTUUID=e91c4e10-16e6-4c0e-bd0e-77becf4a3582 /mnt
sed -i 's/rootfs-a/rootfs-b/g' /mnt/etc/systemd/system/swupdate.service
umount /mnt
# Update rootfs boot parameter in extlinux.conf
sed -i 's/PARTUUID=997046a6-c6f4-4f41-adb4-9fe614b2a92a/
PARTUUID=e91c4e10-16e6-4c0e-bd0e-77becf4a3582/g'
/boot/mmc0_extlinux/stm32mp157f-dk2_extlinux.conf
fi
➍ reboot
列表 9-10:一个脚本,用于准备设备引导更新后的固件
第一个 if 语句 ➊ 检查板卡的 U-Boot 配置文件 stm32mp157f-dk2_extlinux.conf 是否位于 /boot/mmc0_extlinux/,并且是否包含默认的分区 UUID e91c4e10-16e6-4c0e-bd0e-77becf4a3582。如果是,那么 rootfs-a 是活动分区,更新刚刚写入 rootfs-b。因此,新分区上的 swupdate.service 配置文件必须设置为更新即将变为非活动的分区 rootfs-a ➋。
此后,U-Boot 配置文件中的分区 UUID 被替换为代表 rootfs-b 的 UUID ➌。这样可以确保 U-Boot 启动具有新根文件系统的 Linux 内核。如果在此操作之前发生任何故障或发生断电,设备将仅启动现有的固件。但如果一切顺利,根文件系统的分区会被交换,设备会被故意重启 ➍。当然,如果脚本检测到 rootfs-b 是活动分区,反向过程也同样适用。
显然,这只是针对本案例研究中特定架构的后更新脚本的一个可能实现。您的设备在安装固件更新之前或之后,可能需要完全不同的重新配置。
更新过程评估
总结这个案例研究时,我将之前创建的 2GB 镜像写入到 16GB 的 microSD 卡中,并从中启动了我的 STM32MP157F-DK2 板卡。首先,我检查了当前活动的根文件系统分区。Listing 9-11 显示了 A 复制 (mmcblk0p10) 和 B 复制 (mmcblk0p11) 都可用且大小正确。行末的斜杠表示 rootfs-a 当前被挂载为根文件系统。
# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
mmcblk0 179:0 0 14.4G 0 disk
...
|-mmcblk0p10 179:10 0 768M 0 part /
|-mmcblk0p11 179:11 0 768M 0 part
...
Listing 9-11: 挂载为根文件系统的 mmcblk0p10 分区
第二步,我打印了已安装的 swupdate 守护进程的日志。Listing 9-12 显示了输出的部分选定行。
# journalctl -u swupdate.service
➊ Apr 28 21:34:54 stm32mp1 systemd[1]: Started SWUpdate daemon.
...
Apr 28 21:34:54 stm32mp1 swupdate[520]: [INFO ] : SWUPDATE running :
➋ [main] : Running on stm32mp157f-dk2 Revision C02
...
Apr 28 21:34:54 stm32mp1 swupdate[520]: [INFO ] : SWUPDATE running :
➌ [main] : software set: stable mode: rootfs-b
...
Apr 28 21:34:54 stm32mp1 swupdate[520]: [INFO ] : SWUPDATE running :
[start_mongoose] : Mongoose web server version 7.8 with pid 533
➍ started on [0.0.0.0:8080] with web root [/www]
...
Listing 9-12: swupdate 服务日志
结果表明,守护进程启动了➊,并且板卡也启动了,因为硬件版本被正确读取了➋。进一步,显示出期望的配置,即潜在更新应该写入 rootfs-b ➌。此外,包含的 Web 服务器已启动,且配置了端口和指定的目录,得到了确认 ➍。
我使用了一款常见的浏览器,通过 8080 端口连接到设备的 IP 地址,并且立即显示了 SWUpdate 的默认 Web 界面。在这里,可以将 Yocto 创建的 SWU 文件上传到设备,进度条显示了更新完成的百分比。在达到 100% 后,设备如预期重新启动,再次查看 lsblk,如 Listing 9-13 所示,表明从 A 复制到 B 的交换成功。第二次更新尝试也成功,并将根文件系统交换回 mmcblk0p10。
# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
mmcblk0 179:0 0 14.4G 0 disk
...
|-mmcblk0p10 179:10 0 768M 0 part
|-mmcblk0p11 179:11 0 768M 0 part /
...
Listing 9-13: 更新后挂载为根文件系统的 mmcblk0p11 分区
最后,为了测试数字签名验证,我还尝试恶意修改固件更新文件,方法是提取原始文件,将rootfs-a的目标分区更改为/dev/mmcblk0p9,然后将修改后的文件重新合并成一个有效的 CPIO 归档。然而,当通过 Web 界面上传时,它很快就响应了“更新失败”的信息。查看设备上的日志数据,显示签名验证失败,正如预期的那样(列表 9-14)。
# journalctl -u swupdate.service
...
Apr 29 04:03:57 stm32mp1 swupdate[520]: [TRACE] : SWUPDATE running :
[swupdate_verify_file] : Verify signed image: Read 581 bytes
Apr 29 04:03:57 stm32mp1 swupdate[520]: [ERROR] : SWUPDATE failed [0] ERROR :
EVP_DigestVerifyFinal failed, error 0x2000068 0
Apr 29 04:03:57 stm32mp1 swupdate[520]: [TRACE] : SWUPDATE running :
[swupdate_verify_file] : Error Verifying Data
...
列表 9-14:修改后的固件更新的签名验证失败。
测试软件更新验证例程的正确拒绝行为不仅在开发过程中有价值,在发布或生产测试中集成类似的测试用例也非常有意义,因为验证被意外关闭的情况并非第一次发生。
总结
为设备提供软件和固件更新乍看之下似乎不是一项很难的任务。然而,如果考虑到所有关于安全性、可扩展性和可靠性的要求,它就变成了一个复杂的话题,影响着开发流程、非易失性存储布局、后端服务以及客户流程。
本章强调了为各种物联网设备提供安全更新机制的迫切需要,因为没有一款设备是完美的,在某些时候,制造商和客户都会要求进行固件更新,并且这些更新必须以安全的方式分发并应用,且不能导致设备故障。为了满足这些要求,制造商必须确保真实性和完整性保护,必须讨论更新格式和粒度,且内存分区必须支持原子和故障安全的更新过程。此外,还必须运营调度、分发并监控现场更新部署的后端服务器。
如果你仍然觉得这对于你来说太复杂,可能你的设备永远不需要更新,因为在过去的几十年里确实是这样的情况,那么请拿出你的网络设备的风险分析,并重新考虑如果无法修复漏洞时的影响评级。此外,如果你使用的更新机制像是邀请对手安装自定义软件,请确保将“更新误用”添加到你的威胁列表中。
第十章:强健的设备架构

鲁棒性是像汽车、航空航天和工业自动化等行业中组件的关键特性。设备不应受到尘土、寒冷或高温的影响。此类产品被设计成能在较长的生命周期内生存。它们在设计时将物理鲁棒性作为高优先级要求。
然而,随着即便在那些相对保守的领域,连接性和通信的增加,一个新的问题出现了:数字鲁棒性。像基于网络的 DoS 攻击这样的威胁,旨在造成暂时的服务中断,而这对依赖及时系统反应的实时系统来说可能是至关重要的。因此,可用性的保护目标对于这些嵌入式系统变得更加重要。
本章重点讨论了在面对日益增加的网络压力时,连接设备需要具备强大的设备架构。我将讨论实时系统以及嵌入式设备的基本功能受影响的情况。之后,我们将探讨应对嵌入式系统上硬件、操作系统和应用层的 DoS 攻击的策略。本章的案例研究将分析在不同条件下基于 Linux 的嵌入式系统的实时性和鲁棒性表现。
网络压力下的设备
几乎每个设备都有某种网络接口,无论是 Wi-Fi、以太网,还是像 CAN 总线这样的领域特定网络标准。像切断电缆和干扰器阻塞无线通信频道这样的威胁是广为人知的。因为这些威胁导致通信介质的物理可用性丧失,发送过受影响频道的消息将丢失,无法到达最初连接的设备。虽然这个问题会影响整体系统的通信,但设备本身的资源不会受到影响,并且(即使设计得再完美)设备也无法在这种情况下提供帮助。
本章讨论的威胁性质略有不同。它们通过网络连接发起,但与针对通信频道的攻击不同,它们对设备的操作和资源产生影响。这可能是简单的错误数据包发送到错误端口,导致设计不良的工业设备丢失所有功能并进入未记录的更新模式,从而使其控制功能停止。然而,设备被大量信息轰炸的情况,通常才是主要的威胁。
这种设备弱点可能在许多情况下被触发,这些情况随着网络复杂性的增加、运营商引入安全管理流程以及攻击面扩大而变得更加频繁。让我们考虑一些典型的案例。
故障邻近设备
系统越复杂、越异构,设备某天失控并向同一网络中的所有参与者广播大量消息的可能性就越高。持续不断且最终变得高频的流量有可能无意间暴露出连接设备在该领域中的不可靠性。
协议模糊测试
模糊测试是一种安全测试技术,它通过反复生成随机变异的、不规则的输入并将其输入到设备中,目的是暴露出一些未曾考虑到的边缘情况。开发人员通常使用模糊测试来测试他们自己的设备。系统集成商、运营商和研究人员也使用模糊测试来分析设备的鲁棒性与安全性,发现不希望出现的行为。
网络与漏洞扫描
IT 环境通常会定期扫描开放的网络端口、存在漏洞的客户端机器以及配置错误的服务器实例。像工业生产现场这样的环境不太可能受到此类 IT 安全方法的影响,但未来它们也将面临这种情况。各个组件应该能够应对此类扫描,并且不会出现意外行为,例如处理速度的临时波动或设备完全故障。
洪水攻击
一旦攻击者能够访问网络,他们就可以向连接的设备发送数据包。一些工具可以通过一行命令发起基于网络的 DoS 攻击,连脚本小白也能轻松执行。真正强大的设备应该不会受到这些攻击的影响,应该能够继续正常运行。
强大的架构
许多对异常网络通信缺乏鲁棒性的案例都是由于在功能和安全测试中未能发现的错误,这些测试是第一章中描述的安全开发过程的强制性部分。然而,某些设备的 DoS 漏洞来自于你在设计设备时所做的架构决策。
如果你是一个自豪于其坚固产品的工程团队的一员,迈出一步, 同时将数字鲁棒性作为一个高优先级特性来考虑。接下来的部分将提供关于如何构建数字鲁棒性的建议。
设备基本功能
说洪水攻击不应影响设备功能很容易,但实际实现保护措施需要比预期更为详细的考虑。显然,通信能力可能会因高负载的消息流而受到影响,可能会减弱甚至完全丧失,这将影响所有依赖于从其他实体接收数据的设备功能,甚至可能会影响需要将数据从设备传输到其他网络参与者的操作。从设备的角度来看,这些影响是不可避免的,并需要相应的缓解措施。
工业标准 IEC 62443 的第 4-2 部分涵盖了工业组件的技术安全要求,其中包括处理嵌入式系统上的 DoS 保护的特定要求 CR 7.1。它的核心要求是设备即使在 DoS 攻击下并处于降级模式下,也应该保持其基本功能。如果你想满足这一要求,首先要提出的显而易见问题是:“我的设备的基本功能到底是什么?”
让我们考虑三种通用设备类型及其可能的基本功能。
传感器
传感器测量环境参数。不管是追踪温度、距离、pH 值还是填充水平,传感器的任务是捕捉当前情况并将其传递给控制或监控系统。然而,如果通信通道和/或相应的设备资源可能会超载并失去通信能力,传感器仍然应该能够收集其数据。
在这种情况下,基本功能可能是正确地感知并存储所获取的值。从产品工程的角度来看,这可能会导致一个新的需求——即,当通信恢复时,需要一个足够大的数据缓冲区和恢复过程。
执行器
执行器如驱动器、阀门、发动机或甚至激光器会影响物理世界。它们通常由一个中央控制器实例进行参数化,并通过以太网等通信方式与执行器进行通讯。
假设一个驱动器以每分钟 1,000 转的速度运转时,突然发生了对其网络接口的 DoS 攻击。是否应该让其基本功能(以某个速度旋转驱动器)继续运行?从安全性和可用性的角度来看,这可能是合理的。然而,安全专家可能会认为,系统已经不再处于安全状态,应通过停机来避免不安全的行为。
控制器
控制设备用于汽车、工业自动化或关键基础设施,通常遵循图 10-1 所示的简单原理:接收输入,按照给定程序进行处理,可能进行一些通信,然后设置新的输出信号。

图 10-1:控制设备的典型执行周期
随后,周期重新开始。然而,在网络压力的情况下,通信时隙可能会消耗过多的时间和资源。同样,你需要定义自己的策略:可能合理的做法是认为周期执行(除通信外)是应该持续运行的基本功能,即使设备在网络接口上处于压力状态。如果网络通信本身是必需的,问题将是每个周期都绝对需要通信,还是至少每 10 个周期中一次可以接受,例如。
如果定义的要求在运行时无法满足,系统应该可能停机。此类设备的一个好例子可能是读取踏板传感器值并相应控制发动机的汽车组件。这些控制单元的架构师和开发人员必须明确设备的基本功能,以便为传感器通信或相应任务失败的情况做准备——无论是什么原因导致的。
注意
定义设备的基本功能是一个基础性的设计决策,这个决策可能对设备的软件和硬件架构产生重大影响。务必在早期认真考虑这一主题。
实时系统
数字鲁棒性对于几乎所有产品都很重要,因为设备故障往往会导致运营商的财务损失。然而,对于实时嵌入式系统来说,这一点尤其重要。这些设备不仅要求保持服务的可用性,还必须确保在初始事件发生后,设备操作的结果能够在给定的截止时间之前提供。
尽管高性能处理器和超快反应时间可能是此类需求的结果,但这些特点并非强制要求。唯一重要的约束条件是,系统的响应必须在时间限制结束之前提供,无论是微秒还是半分钟。
实时系统有三类,它们在错过截止时间后的影响程度上有所不同:软实时系统、严实时系统和硬实时系统。
软实时系统
软实时系统即便经常错过截止时间也能容忍。然而,延迟结果的有用性和价值会持续下降。如果错过的截止时间较为严重,结果可能会变得毫无价值。应用实例包括气象站、实时音频传输和视频游戏。
严实时系统
在严实时系统中,错过的截止时间会导致缺陷或服务质量下降。然而,偶尔错过截止时间可能是可以容忍的。这类系统的一个例子可能是取放机器人。如果它的控制器错过了截止时间,当前处理的部件可能会损坏或放置错误,但在处理完这一单一故障后,设备能够继续正常操作。
硬实时系统
硬实时系统必须满足最高的要求。对于这些系统,任何错过的截止时间都是至关重要的。根据应用场景的不同,错过截止时间甚至可能导致灾难性的后果。典型的例子包括飞机或火车上的发动机控制单元、高质量制造过程,以及像起搏器这样的医疗设备。
DoS 攻击的影响
如果您的设备属于这些实时类别中的某一类,您必须在威胁和风险评估过程中考虑 DoS 攻击或偶发的网络压力带来的潜在影响,并相应地缓解由此产生的弱点。
资源耗尽与预防策略
基于网络的 DoS 攻击,如洪水攻击,旨在对目标设备造成临时资源耗尽。在一个简单的场景中,攻击者可以通过利用大部分可用的网络带宽向目标设备发送数据包来实现这一点。受害设备接收所有数据包并必须处理它们。所需的处理能力取决于网络数据包的类型和内容。
然而,如果网络数据包的积累达到填满所有内部缓冲区的程度,并且设备无法比数据包到达更快地处理它们,就会发生 CPU 资源耗尽。此时设备无法再执行队列任务,包括其基本功能。
警告
资源耗尽的原因不一定是恶意攻击。一次粗心的网络扫描,使用 nmap 可能就足以暂时引发设备的资源耗尽,正如本章案例研究所示。
完全预防资源耗尽在实践中并不总是可能的,它取决于特定设备的参数和攻击者的能力。然而,基本功能和次要操作之间的强大分离是一个稳固的架构,可以在遭受显著网络压力的情况下保持降级模式下运行。通过使用两种基本策略,可以将设备资源分配到关键操作和相对不太关键的操作中:
固定资源分配 资源静态分配给任务的优点在于提供了透明且清晰的职责划分。然而,固定的资源限制会在运行时导致低效,当资源没有被其分配的任务使用时,另一个任务本可以使用该资源,但由于限制无法使用。
动态资源分配 为了促进资源的高效使用,可以根据任务属性在运行时将资源分配给特定任务,这些任务属性用于推导优先级指标。
在设计设备硬件架构和选择主要处理单元时,应考虑这些方法。此外,操作系统的选择和配置对设备的鲁棒性也有重要影响。
硬件级实现选项
设备的硬件架构定义了其基本的计算资源条件。在开发过程中,选择了各种目的的集成电路(IC),包括微控制器、多核系统级芯片(SoC)和 FPGA,以及专用芯片,如处理物理层网络通信的以太网物理层(Ethernet PHY)。
如果网络洪水的鲁棒性是你的主要关注点之一,并且你在开发过程的早期就考虑到了这一点,你可以设计设备的硬件架构,从而显著减少基于网络的拒绝服务(DoS)攻击的风险。
专用预处理单元
图 10-2 展示了在引入专用预处理单元处理网络流量时的基本概念。该架构的主要思想是物理上将主应用处理器和网络处理分开,或至少将其中一部分分离开来。

图 10-2:带有网络流量预处理的架构
这种架构的一个具体例子是通信预处理器,它实现了 TCP/IP 协议栈并负责处理网络包及其负载。主处理器与预处理单元之间的相关数据通信通过一个尽可能简单的接口进行。遇到洪水攻击或由于未知原因造成的非常高的网络负载时,通信单元可能会出现资源耗尽,但其背后的应用处理器不会受到影响,仍然能够执行设备的基本功能。
使用硬件包过滤单元是这种方法的第二个例子。在这种情况下,网络栈保留在主处理单元上,预处理器仅用于过滤到达设备网络接口的网络包流。过滤规则可能包括速率限制,确保应用处理器只接收它能够处理的包数量。
这种架构可以通过提供此功能的特定网络集成电路(IC)来实现,但在写作时,这些 IC 在嵌入式系统中还相对较少见。对于托管 FPGA 的设备,MAC 控制器可以通过数字逻辑实现,然后在将网络数据传递给主 CPU 之前,经过定制的包过滤核心。
警告
在设计主处理器与预处理单元之间的通信时要小心。如果前者对后者的依赖过大,即使架构坚固,设备也可能会出现故障。
多核架构
如果数字鲁棒性要求在需求列表上出现得太晚,而硬件设计已经确定,那么如果你选择了多核 SoC 作为设备的主要处理单元,你可能会比较幸运。诚然,多核芯片中的单个核心并不是完全分离和独立的,它们共享总线、缓存等资源。然而,正如图 10-3 所示,合理的任务分配可以降低 DoS 事件的概率。

图 10-3:使用多核架构提升系统鲁棒性
越来越多的嵌入式系统 SoC 实现了异构多处理(AMP)。它们包括一个或多个高性能核心,用于运行功能丰富的操作系统,如 Linux,还有一个或多个较小的核心,通常擅长实时应用。增强鲁棒性的一种方法可能是将控制任务等关键功能分配给专用的实时核心,如图 10-3a 所示。类似的结构在将硬处理器与位于 FPGA 架构中的软核心结合的 SoC 上也可以实现。
如果你的设备运行在对称多处理(SMP) SoC 上——这意味着它有两个、四个、八个甚至更多相同类型的核心——你仍然可以采取措施提高鲁棒性。图 10-3b 中的架构旨在通过将网络任务的进程绑定到专用核心来将其与关键功能分离。这种概念称为核心绑定或处理器亲和性,并且必须得到操作系统的支持。
警告
再次强调,(通信)在不同核心上的进程之间的依赖关系可能会破坏预期的分离,应该谨慎地进行概念化。
操作系统功能
操作系统的一个核心任务是管理 CPU 核心、内存区域以及多种硬件接口。负责在运行时分配处理时间的组件被称为调度器。在讨论防止资源耗尽时,考虑操作系统和调度器的功能和职责是完全合理的。
操作系统选项
在选择操作系统时,设备工程师基本上有四种选择:裸机软件(无操作系统)、实时操作系统、完整的操作系统或基于虚拟机的操作系统组合。
裸机软件(无操作系统)
在简单的设备和应用中,操作系统甚至可能不是必需的。事件及其相应的资源分配可以通过轮询循环或中断服务程序来处理。
实时操作系统
实时操作系统(RTOS)是一种低复杂度的操作系统,专门用于对可靠性有高度需求的实时应用。它的调度器根据任务优先级来管理资源:优先级较高的任务可以中断当前运行的优先级较低的任务。
在这样的系统中,重要的是不要将与网络相关的任务配置为最高优先级,因为这样可能会导致网络引发的 DoS 情况。实际上,在该领域可以找到各种 RTOS,包括商业的 QNX 或 VxWorks,也有开源的变种,如 FreeRTOS、RIOT 或 Zephyr。
完整操作系统
许多现代功能丰富的设备依赖于开源库和工具,处理例如网络通信、用户界面展示或数据处理任务。这些设备通常基于像 Linux 这样的完全功能的操作系统。然而,这类复杂且不完全确定性的操作系统有一个缺点:缺乏适当的实时能力。在下一节中,我将讨论一种使 Linux 更具实时能力的可能性。
基于虚拟机监控器的操作系统组合
一些制造商希望将实时操作系统(RTOS)的实时优势与 Linux 的众多库和功能结合起来。在这些情况下,在这些操作系统下引入了一个额外的抽象层:一个虚拟机监控器负责硬件资源分配的任务,从而在将核心功能(可能运行在 RTOS 上)与在 Linux 中实现的支持功能分离方面发挥着重要作用。这些配置进一步增加了产品软件架构的复杂性。
带实时补丁的 Linux
在桌面和服务器系统的典型配置中,Linux 并不提供可靠的实时能力。然而,多年来,实时社区一直维护着一个名为PREEMPT_RT的内核补丁。它对 Linux 内核进行了多项修改,目的是实现对内核线程和内核原语的抢占,这意味着任务调度中的非确定性减少,计算资源分配更接近纯粹基于优先级的系统。在当前的 Linux 内核版本中,您可以通过 CONFIG_PREEMPT_RT 配置选项来启用实时支持。
Linux 中的线程调度是基于调度策略和静态调度优先级进行的。“正常”线程按照像 SCHED_OTHER、SCHED_IDLE 或 SCHED_BATCH 这样的策略调度,优先级为 0。必须满足实时要求的线程会根据以下策略之一进行调度:
SCHED_FIFO 该调度程序遵循先进先出(FIFO)原则,意味着正在运行的线程会一直执行,直到被一个具有更高静态优先级的线程抢占。
SCHED_RR 经典的轮转调度(RR)方法类似于 SCHED_FIFO,不同之处在于线程只能运行一定的最大时间。之后,线程被中断并添加到具有相同优先级的线程队列的末尾。具有更高优先级的线程可能会抢占较低优先级的线程。
SCHED_DEADLINE 与其他调度程序不同,这个调度程序基于全局最早截止时间优先(GEDF)。它不依赖于静态优先级,而是动态分配优先级。其决策基于线程的绝对截止时间和总计算时间。
注意
尽管 PREEMPT_RT 使 Linux 更像实时操作系统,但它仍然是一个复杂的软件,不适合许多硬实时需求。
应用与协议考虑事项
本章主要关注硬件和操作系统层面上稳健的设备架构,但我并不想忽视应用层和通信层。现实世界中,许多 DoS 情形源于软件和协议设计的不严谨。
这些问题可能在开发过程的不同阶段引入。协议规范可能已经缺乏对极端情况正确行为的明确定义——例如,消息参数的最小值或最大值。此外,软件中的简单错误,比如缺少边界检查,也可能在特定情况下导致不希望的副作用,如无限循环或死锁。如果仅进行表面上的安全测试,设备就可能带着固件进入生产,而这些固件给攻击者留下了通过单个网络数据包触发 DoS 事件的机会。
以下章节提供了解决这一复杂问题的实用建议。你应该关注的一个重要可能性是攻击者能够强制设备状态转变,从而导致拒绝服务(DoS)攻击。
识别逻辑缺陷
协议设计是一项复杂的任务,但许多公司选择这条路,开发自己的专有消息和通信格式。如果你发现自己处于这种情况,一定要尽力消除协议中的逻辑缺陷。
首先,你可以从概念层面开始,提出以下问题:我们消息中每个值的有效范围是什么?网络参与者能否强制我们的设备进入未定义的状态?在使用消息之前,是否验证了消息值的有效性和合理性?其次,使用仅部分满足规范或甚至故意尝试操控设备状态的消息来测试你的协议实现,可以帮助发现漏洞。
实施输入和发送者验证
在许多嵌入式系统场景中,攻击者能够直接与设备通信——例如,通过网络。即使应该使用官方客户端软件与设备进行交互,攻击者也可能会自行构造被篡改的消息,因此设备不应信任任何到达其网络接口的数据包。
关键命令,如切换到更新模式和停止所有操作,只有在成功验证发送者的身份后才能执行。因此,开发者的默认态度应该是预期恶意输入,并相应地实现合适的输入验证和过滤机制,无论是在防火墙层面还是在处理消息有效载荷的应用程序内部。
分析主动保护措施
有时候,攻击者可能会滥用主动保护措施,将设备强制置于拒绝服务(DoS)状态。例如,你的登录过程可能会被加固,以通过仅允许 10 次登录尝试来防止暴力破解攻击。达到该限制后,设备会进入锁定模式,只有通过恢复程序才能重新激活。虽然这可能是一个完全合理的安全措施,但它也允许攻击者通过输入错误的用户凭证 10 次来强制产生 DoS 状态,即便是合法用户也会受到影响。
基于流量数量阻止 IP 的防火墙配置也有类似的机制。在这种情况下,攻击者可能会伪造有效设备的发送者 IP 地址,通过发送大量数据包来触发保护功能。结果,合法设备的通信尝试随后会被阻塞。
引入混沌工程与模糊测试
人类的想象力是有限的。假设开发人员能预见到所有可能导致设备 DoS 事件的问题是不现实的。然而,有两种测试方法可以在这方面突破极限。
混沌工程是一种针对 IT 系统的可靠性和韧性测试,通过在 IT 基础设施中引入“混沌”——即随机失败的服务或应用程序,来进行测试。在嵌入式系统测试领域,这种“混沌”可以表现为崩溃的进程或失去的通信通道,从而分析设备在不利环境下的行为。
模糊测试的学科可以应用于通信协议,也可以应用于设备处理的输入数据,如配置文件或证书。它可以通过巧妙地多次变异输入数据,创建大量的测试消息或测试文件。通过这种方式,你可以发现那些通过人工分析非常难以发现的、会导致 DoS 状态的情况。
注意
话虽如此,请记住,产品开发团队的思维方式和意识,可能是早期发现 DoS 漏洞的决定性因素。
案例研究:STM32MP157F 设备的鲁棒性选项
在本案例研究中,我将分析 STM32MP157F 设备的实时性和鲁棒性能力。我将阐明用于模拟设备 CPU 和网络压力的相应测量方法和工具。
基本系统属性
在硬件级别选项方面,当前使用的 STM32MP157F 设备提供了两个运行在 800 MHz 的 Cortex-A7 核心和一个运行在 209 MHz 的专用 Cortex-M4 核心,用于实时应用。这个基本信息使我们可以得出结论:既可以将任务绑定到 A7 核心之一,也可以将关键软件应用程序移到分离的 M4 核心。原则上,使用 M4 核心作为以太网流量的预处理单元甚至可能是一种选择,但这可能不是我首先会考虑的方案,且需要基于 SoC 架构进一步进行可行性分析。
STM32MP157F 的以太网外设中包含的 MAC 单元支持 10、100 和 1,000Mbps 的数据传输速率。在查阅了 ST 针对 STM32MP157F 设备的参考手册 RM0436之后,我们可以清楚地看到该模块还支持硬件加速的数据包过滤。接收到的数据包可以根据其源和目标 MAC 地址、以太网帧中的虚拟局域网(VLAN)标签、源和目标 IP 地址以及 TCP 和 UDP 消息的源和目标端口进行过滤。不幸的是,该模块没有任何形式的速率限制功能,这在 DoS 保护中会非常有用。
本次初步分析的第三部分涉及我 STM32MP157F 设备上运行的 Linux 操作系统。如前所述,Linux 可以通过补丁或配置使其行为类似于实时系统。通过列表 10-1 中的命令和输出,我检查了我的 Linux 系统,它是基于 ST 的st-image-core镜像通过 Yocto 创建的,是否具备实时调度功能。
# uname -a
Linux stm32mp1 5.15.67 #1 SMP PREEMPT ...
列表 10-1:我系统的 Linux 内核属性
字符串SMP指出,当前的 Linux 系统在编译时已配置为支持对称多核架构,如 STM32MP157F 的 A7 双核,而PREEMPT则表示 Linux 内核是通过CONFIG_PREEMPT选项编译的。该系统运行一个低延迟内核,这意味着内核代码在非关键区段执行时,可以被更高优先级的任务中断。然而,这种配置不应与PREEMPT_RT指示符混淆,后者代表的是通过CONFIG_PREEMPT_RT启用的完全可抢占 Linux 内核。
低延迟内核的测量
你可以通过两种方式来测量设备的延迟行为。如果你可以访问设备的 Linux 系统,你可以运行分析该 Linux 系统调度行为的软件。另一方面,例如,如果你分析一个第三方组件,你可能不得不将待测设备视为一个黑箱。在这种情况下,你只能通过输入和输出信号来观察设备的延迟行为——例如,通过示波器或逻辑分析仪。前一种方法更适合展示设备的基本能力,而后一种方法可以得出更接近特定应用场景的结果。
在这个案例研究中,我使用 cyclictest 工具来分析系统的实时能力,因为我可以访问设备的 Linux 控制台,并且我目前没有特定的应用程序。该工具测量使用 SCHED_FIFO 调度的实时任务的编程执行和实际执行之间的延迟。清单 10-2 显示了一个测试样本及其结果。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 874) P:80 I:200 C: 38735 Min: 15 Act: 47 Avg: 36 Max: 158
T: 1 ( 875) P:80 I:200 C: 38596 Min: 15 Act: 42 Avg: 35 Max: 165
清单 10-2:空闲模式下的任务延迟
--mlockall 参数用于减少工具本身的开销和影响,而 --smp 则是多核系统测试所必需的。测量线程按照 --interval 选项设定的每 200 µs 执行,并且不同线程的测量周期没有区别,正如 --distance=0 所表示的那样。测量任务执行了 40,000 次(--loops=40000),任务优先级为 80(--priority=80)。
结果输出显示了两行,每行对应一个 CPU 核心。字母 P、I 和 C 分别表示优先级设置、测量间隔和执行测量的次数。右侧的值显示了最小、实际、平均和最大观察到的延迟。最右边的数字是最重要的,因为它表示对计划任务的最差延迟,这可能对你来说是可以接受的,也可能是不可接受的。
在当前状态下,给定的 Linux 系统使用 PREEMPT 低延迟内核显示出最大延迟为 158 和 165 µs。然而,如果我使用同一网络上的第二个设备进行 SYN 洪水攻击——例如,运行 hping3 --syn --flood device-ip,最大延迟会显著受到影响,如清单 10-3 所示。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 902) P:80 I:200 C: 14253 Min: 15 Act: 2406 Avg: 409 Max: 5475
T: 1 ( 903) P:80 I:200 C: 40000 Min: 15 Act: 15 Avg: 23 Max: 185
清单 10-3:SYN 洪水对任务延迟的影响
在这种情况下,即使使用低延迟内核,任务延迟也可能上升到超过 5 毫秒,这是实际任务间隔的多倍。显然,如果你的应用程序必须满足实时要求,这可能是至关重要的。清单 10-4 显示了在执行简单端口扫描时使用 nmap device-ip 得到的测量结果。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 932) P:80 I:200 C: 37581 Min: 14 Act: 41 Avg: 46 Max: 6573
T: 1 ( 933) P:80 I:200 C: 39086 Min: 15 Act: 46 Avg: 34 Max: 199
清单 10-4:SYN 扫描对任务延迟的影响 nmap
总结来说,我们可以看到,针对 STM32MP157F 设备的标准 Linux 发行版的低延迟内核无法提供强大的实时特性。即使是短时间的强烈网络流量也会影响系统的反应时间。
实时内核的测量
如果稳健性是你的主要需求之一,你可能需要寻找实现了完全可抢占内核的 Linux 发行版,以满足实时约束。幸运的是,ST 为 STM32MP157F 设备提供了 Yocto 层 meta-st-x-linux-rt。将其添加到我的构建系统后,我只需设置 MACHINE=stm32mp15-rt 并重新创建构建环境,然后就可以使用 bitbake 生成启用了 CONFIG_PREEMPT_RT 的 st-image-core 镜像和 Linux 内核。
启动新镜像后,我再次确认内核实际上提供了实时功能。列表 10-5 中的输出显示了典型的 PREEMPT_RT 特性,如所期望的那样。
# uname -a
Linux stm32mp15-rt 5.15.67-rt49 #1 SMP PREEMPT_RT ...
列表 10-5:包括实时功能的 Linux 内核特性
再次,你可以像上一节那样使用cyclictest测试系统的实时表现。列表 10-6 显示了实时内核的正面效果。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 1195) P:80 I:200 C: 40000 Min: 15 Act: 17 Avg: 19 Max: 64
T: 1 ( 1196) P:80 I:200 C: 39838 Min: 16 Act: 25 Avg: 18 Max: 72
列表 10-6:空闲模式下实时内核的任务延迟
与列表 10-2 中低延迟内核的值(158 和 165 µ秒)相比,最大延迟显著降低至 64 和 72 µ秒,分别是。 列表 10-7 中的结果显示了更好的表现。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 1186) P:80 I:200 C: 40000 Min: 15 Act: 29 Avg: 24 Max: 91
T: 1 ( 1187) P:80 I:200 C: 39800 Min: 16 Act: 34 Avg: 19 Max: 77
列表 10-7:SYN 洪泛对实时内核延迟的影响
即使在进行 hping3 执行的 SYN 洪泛攻击时,Linux 系统的最大延迟也不会超过 100 µ秒。
你可以在列表 10-8 中观察到类似的效果,该结果是在测试设备上运行网络扫描时捕获的。
# cyclictest --mlockall --smp --interval=200 --distance=0 --priority=80
--loops=40000
...
T: 0 ( 1189) P:80 I:200 C: 40000 Min: 15 Act: 17 Avg: 20 Max: 78
T: 1 ( 1190) P:80 I:200 C: 39837 Min: 16 Act: 23 Avg: 19 Max: 73
列表 10-8:使用 nmap 进行 SYN 扫描对实时内核延迟的影响
虽然常见的 nmap 命令会使标准镜像的低延迟内核引发超过 6 毫秒的最坏情况延迟,但本节分析的实时内核只会使最大延迟比空闲状态增加不到 15 µ秒。
这意味着,低延迟 Linux 内核并不保证任何稳健性,即使是完全可抢占的 Linux 内核也只能接近实时表现。如果这是一个可行的选项,务必理解切换到实时 Linux 内核时,你是以牺牲性能换取确定性。此外,为了实现实时行为,诸如 SoC 的动态功率管理和频率缩放等特性可能会被禁用。
注意
所有展示的最大延迟测量值都应视为粗略估算。你的设备可能会遇到更糟糕的情况,导致更高的最大反应时间。
实时协处理器
如果你想满足硬实时需求,Linux 可能不是最佳选择。然而,STM32MP157F 提供了一个额外的 Cortex-M4 核心,正是为了这个目的。专用 M4 固件的开发和构建过程超出了本案例研究的范围,但你可以使用 ST 的 STM32CubeIDE 或你最喜欢的定制 Makefile 来实现这一目标。
假设你已经创建了一个包含实时应用程序的固件文件 m4_fw.elf。通常,你会将该文件放在 /lib/firmware/ 目录中,因为它是协处理器的固件。清单 10-9 显示了 Linux remoteproc 框架的基本初始化,以准备 M4 固件的执行。
# echo -n "/lib/firmware/" > /sys/module/firmware_class/parameters/path
# echo -n "m4_fw.elf" > /sys/class/remoteproc/remoteproc0/firmware
清单 10-9:使用 remoteproc 框架初始化 M4 固件
在第一步中,固件路径 /lib/firmware/ 被写入相应的 sysfs 节点。随后,特定固件文件的名称(在此案例中为 m4_fw.elf)会传递给远程处理器实例 remoteproc0,代表 M4 实时核心。
此时,什么都没有运行。清单 10-10 中的输出确认 M4 仍然处于离线状态。要启动提供的固件,必须将 start 关键字写入 remoteproc0/state。
# cat /sys/class/remoteproc/remoteproc0/state
offline
# echo start > /sys/class/remoteproc/remoteproc0/state
... remoteproc remoteproc0: powering up m4
... remoteproc remoteproc0: Booting fw image m4_fw.elf, size 456520
... remoteproc0#vdev0buffer: assigned reserved memory node ...
... virtio_rpmsg_bus virtio0: rpmsg host is online
... remoteproc0#vdev0buffer: registered virtio0 (type 7)
... remoteproc remoteproc0: remote processor m4 is now up
# cat /sys/class/remoteproc/remoteproc0/state
running
清单 10-10:使用 remoteproc 框架启动 M4 固件
随后,核心被启动,实时应用程序的执行开始。即使 Linux 系统遭遇高网络负载并且资源耗尽,实时固件也会继续以其 CPU 时钟频率 209 MHz 不受干扰地运行。同时,remoteproc0 的状态已相应更改为 running。
如 清单 10-11 所示,Linux 系统可以通过写入 stop 到 remoteproc0/state 来停止固件的执行。
# echo stop > /sys/class/remoteproc/remoteproc0/state
... remoteproc remoteproc0: warning: remote FW shutdown without ack
... remoteproc remoteproc0: stopped remote processor m4
清单 10-11:使用 remoteproc 框架停止 M4 固件
如果你的应用需要,Linux 的远程处理器消息(RPMsg)框架可以在主 CPU 和协处理器之间交换信息。然而,确保避免实时应用对 Linux 系统的强依赖,因为这可能会导致整个设备再次发生故障。
总结
许多行业将鲁棒性视为所有设备的基本特性。然而,当涉及到高度互联和自动化的系统时,客户和制造商通常很难明确规定他们对数字鲁棒性的理解。
本章弥合了嵌入式系统的实时世界与可用性安全保护目标之间的差距。设备工程师和架构师应当从中得到的最重要的启示是,他们必须考虑网络和其他接口上可能的数字压力、即使在遭受攻击时也应正常工作的设备核心功能,以及支持真正实时行为的架构决策。
实现稳健设备架构的选项从集成专用硬件资源到在多核 SoC 中分离职责,再到精心选择和配置设备操作系统不等。但正如本章案例研究所示,没有什么能够替代在空闲状态和潜在压力情况下对设备行为的实际评估。
第十一章:访问控制与管理**

在 IT 系统中,用户和权限的管理有着悠久的历史,因为从一开始,人类就在其中扮演了重要角色。相比之下,嵌入式系统过去并不打算进行交互,通常只有少数用户,甚至只有一个系统用户。
今天,随着物联网商业模型和应用场景的日益复杂,许多参与者都涉及到物联网设备的生命周期过程——从开发者、维护人员、第三方服务提供商到最终用户。设备必须能够处理这些不同的角色,并将它们分离开来。此外,设备上运行的内部过程和应用程序也需要各种权限来完成其目的。按照最小权限原则限制这些权限,可以避免设备遭受严重损害。
本章将从多个角度讨论访问控制如何对设备安全做出重要贡献。接下来,我将介绍你可以用来在运行 Linux 的设备上实现访问限制的常见概念。章末的案例研究则展示了如何使用 AppArmor 工具对进程进行限制的实际可能性。
日常威胁
数据库泄露和漏洞几乎每天都会发生。犯罪分子发布或出售的数据往往包含大量的用户名和密码。你的物联网设备的凭证可能也在这些泄露的秘密中,使得攻击者能够登录到你的产品。你可能会得出结论,认为这些是客户的风险,并且在凭证被盗的情况下,你不需要承担任何责任,但这并不完全正确。
你至少需要问自己两个问题:“我们是否将最终用户账户与制造商账户分开?”以及“我们是否尽可能限制这些最终用户账户,以便在凭证泄露时控制损害?”如果你不考虑这些问题,它们可能会反弹,至少会对你产品的声誉产生负面影响。
渗透测试人员、安全研究人员,甚至是客户,都会定期发现产品中的未知漏洞。随着设备和软件复杂度的增加,以及网络接口上暴露的服务数量的增加,安全问题的概率也会增加。即使你已经建立了一个稳健的漏洞管理流程,如第一章所述,并且准备好以安全的方式发布更新,如第九章所解释的那样,你的产品在某些时间段内仍然可能会有漏洞。如果这种情况导致在实际应用中发生攻击,严重影响设备,专家们自然会问:“为什么这个有漏洞的应用程序在其目的相对有限的情况下,竟然能够影响系统的所有部分?”
当前,多个行业的制造商普遍讨论的一个话题是将他们的设备转变为平台,可以从相应的市场安装并运行应用程序,而这些市场又由全球的应用程序开发者提供支持。同样,可能看起来显而易见的是,这些开发者负责其应用程序的安全。然而,如果应用程序中实际存在的漏洞被利用,那么保护系统进程和配置以及其他应用免受问题应用影响的责任将落在平台设计者身上。
有时,访问控制只在软件或操作系统层面上被考虑。然而,对于嵌入式系统来说,另一个额外的威胁具有重要的相关性:物理访问。攻击者以及渗透测试人员和研究人员可以通过与本地(调试)接口如 JTAG、通用异步接收器-发射器(UART)或互连电路(I²C)总线交互来对设备进行物理分析。
通过声称“没有人会打开我们的设备”,甚至如果真有人打开,“内部结构如此复杂,就连我们的工程师都不了解所有细节”来减轻这种威胁是常见的,但这种说法很少是合理的。有兴趣的攻击者肯定会拆开产品外壳并寻找本地接口,如果他们有一定动机,他们也会愿意花时间手动进行设备反向工程,直到达到他们的目的。
访问控制与损害隔离
在许多情况下,完善的访问控制管理可以减少甚至防止攻击的影响。这是一个完美的例子,展示了工程师和开发者如何严肃对待深度防御原则。如果一个安全层(如用户凭证的机密性)失败,或发现未知的软件漏洞,访问控制层将介入并防止最严重的后果。
然而,这只有在你有一个合理的基础来决定是否应当授予访问权限,以及在授予权限时,实际需要多少访问权限的情况下才有效。在授予用户访问文件或硬件资源等对象的权限时,你应该考虑几个属性。
权限可以根据用户身份授予或拒绝,这种方法有时被称为基于身份的访问控制(IBAC)。然而,独立地管理每个用户或人可能过于复杂。因此,基于角色的访问控制(RBAC)应运而生,它根据用户的角色来设定权限。这不仅简化了权限管理,而且要求你明确地将角色分配给每个用户,从而提高了透明度。
另一种访问控制方法依赖于主体、对象,甚至可能是它们的环境的属性。这种方法被称为基于属性的访问控制(ABAC),它比 IBAC 或 RBAC 能做出更动态的访问决策。
优化安全性方面的访问控制的常见策略是将权限减少到给定用户或应用程序所需的最低权限。然而,这正是问题的关键,因为这些最低要求通常并不明确知道。
因此,开发人员通常倾向于慷慨地设置权限,给恶意活动或入侵留下空间。他们的理由是可以理解的,因为过度限制用户和应用程序可能导致设备无法操作。而且,情况可能更糟的是,用户的职责和应用程序的访问权限可能随着时间的推移而发生变化。因此,重要的是要尽早并全面地考虑访问控制管理,贯穿整个设备生命周期。
设计与开发阶段
在硬件设计过程中,关于对 IC 引脚、焊盘和 PCB 上的迹线的物理访问的讨论应该已经提上议程。调试端口的模糊化或在关键部件上应用环氧树脂可能是需要考虑的解决方案。通过接触开关或弹簧,产品外壳被打开时向主处理器发出信号,也可能减少物理访问的影响。
为了提高安全性,甚至可以将导电网格结构集成到塑料外壳中,持续运行信号以检测外壳是否遭到篡改。此类机制已在支付终端中得到应用。
固件开发的重要部分是系统用户、他们的角色、目录、初始文件和相应权限的规范。即使这听起来微不足道,也要确保考虑到设备预期使用情况的整个范围。结果必须在你选择的构建系统中实现(例如 Yocto 或 Buildroot),并且在整个开发过程中应监控其正确性。
接下来,必须分析设备的所有软件应用程序和服务。一方面,必须指定每个应用程序应该在哪个用户上下文中运行。这定义了进程在运行时拥有的权限,当进程被恶意行为者接管时,这一点尤为重要。仔细考虑是否 root 总是最好的选择。
另一方面,应用程序也可以在访问控制管理中发挥积极作用。以 Web 服务器为例。这种常见的应用程序控制它为连接的客户端提供的网页。某些网页界面部分,如设备管理页面,可能比其他部分更为关键。基于操作系统用户或 Web 服务器自己的用户管理,服务器必须适当配置,只允许合法用户访问管理员权限。
当然,应用程序可能有多个“管理员”用户:一个用于网页应用管理任务的webadmin,一个用于 Linux 系统管理的sysadmin,还有一个可能用于制造商访问的superadmin。这些用户都有不同的权限。
生产考虑事项
设备生产通常不被视为访问控制管理中的重要步骤,但越来越多的标准——例如欧洲的 ETSI EN 303 645 消费电子标准——要求设备避免实施全球默认密码。由于设备的固件通常是一个静态的全球性工件,因此必须在生产过程中生成并设置设备专有的密码。
不仅仅是固件需要个性化。纸张、标签或产品包装的一部分也需要标明个别密码。这个过程很大程度上依赖于设备的固件结构和生产过程。
生产过程中密码生成的替代方案是强制终端用户在首次登录时设置新的自定义密码。这种方法的优点是可以使用单一的全球固件镜像,这在生产过程中更容易处理。然而,这样做也意味着你的产品仍然会有一个可以在初始化时至少使用一次的通用默认密码——无论是用户还是攻击者都可以使用。
客户活动与报废
在现场,客户可能希望自行创建额外的用户或更改已有账户的权限。你必须决定是否允许这样做,如果允许,应在什么范围内。允许客户自由选择授予哪些权限可能导致权限提升,从而使最终用户获得比原本预期更多的权限。
手动更改数百台设备的密码绝对是枯燥乏味且容易出错的。因此,现代物联网基础设施需要对资产和配置(包括用户、角色和权限)进行集中管理,以确保能够保持可管理性。对于设备制造商来说,准备设备以支持轻量级目录访问协议(LDAP)集成或类似的技术可能是合理的,以简化集中管理。然而,确保在信任远程服务器来处理设备的用户、凭证和权限时,要考虑潜在的威胁。
如前所述,应用程序应被限制为仅具有正确运行所需的最小权限。然而,随着每次固件更新,设备软件组件的功能和行为可能会发生变化。新的功能可能需要更多的权限,而严格的安全性可能会成为阻碍。同时,安全更新可能会删除那些曾经需要特定权限的软件程序,这些权限之后就不再需要了。一般来说,如果您使用的是限制软件应用程序的工具并且更新了该软件,您总是需要再次检查相关权限是否与新版本匹配。
最后但同样重要的是,您必须考虑到您的设备可能会被转售并被另一位客户使用。那位客户应该仍然能够初始化设备并重置用户和权限,但不应访问前一位所有者创建的数据。同样的情况也适用于最终停用,当好奇的垃圾桶潜伏者或废品商贩可能想要获取访问受限的数据时。应该为原始客户提供一个“清除所有私人数据”按钮,确保该按钮真正执行它所说的功能,并且新添加的用户即使没有清除数据,也不能访问其他人的数据。
自主访问控制
自主访问控制(DAC) 是一种管理系统内对象的主题和组的访问权限的基本方法。在我们的例子中,主题是 Linux 操作系统中的用户,而对象可能是文件、目录、内存位置、进程间通信以及各种系统设备和接口。此方法是 自主的,因为某个对象的权限是由拥有该特定对象的用户 自行决定 的;所有者 可以将权限传递给其他用户和组。
注意
在 Linux 系统中,root 用户是无所不能的。它能够覆盖所有者设置的权限。因此,“成为 root”是攻击者最具吸引力的目标之一。对于开发者而言,这意味着可能被破坏的进程(例如所有监听网络端口的应用程序)不应该以 root 身份运行!
Linux 文件系统权限
每个 Linux 文件系统中的文件都有一个与之关联的权限字符串。如 Listing 11-1 所示,ls 工具可以在每一行的开头始终打印出这个字符串。
# ls -l
-rw-r--r-- 1 root root 23 Apr 30 13:13 README
lrwxrwxrwx 1 root root 17 Apr 30 14:15 current_logfile -> logs/73944561.log
drwxr-xr-x 2 root root 1024 Apr 30 14:14 logs
-rw-r--r-- 1 bob guest 12 Apr 30 17:15 my_notes.md
# ls -l logs/73944561.log
-rw-r--r-- 1 root root 9 Apr 30 12:20 73944561.log
Listing 11-1: Linux 上的文件权限列表
显眼的字符串 root root 表明所有文件都归 root 用户和 root 组所有,除了 my_notes.md 文件,它归 guest 组中的 bob 所有。此外,每行的第一个字符指定文件类型,是普通文件(-)、目录(d)还是符号链接(l)。块设备(b)和字符设备(c)也会在那里显示。接下来的九个字符代表每个文件的权限,它们被分为三组,每组三个字符,分别表示所有者、相应组和其他人对读取(r)、写入(w)和执行(x)的权限。
例如,清单 11-1 中的 README 文件可以被 root 用户读取和写入,但其他 root 组中的用户只能读取。读权限也被授予任何其他用户,无论其所在组如何,这由后缀的 r-- 所示。
每当你阅读关于 Linux 访问控制管理的教程或第三方源代码时,你可能会遇到一种高效的三位数字访问权限表示法。9 位的权限字符串可以高效地写成三个八进制数字:一个表示所有者权限,一个表示组成员权限,最后一个表示其他所有人的权限。
例如,777 表示每个人都可以读取、写入和执行给定对象。相对的,740 表示所有者可以读取、写入和执行;组成员仅被允许读取;而其他任何人都没有访问权限。表 11-1 详细说明了权限到八进制数字的转换及其反向转换。
表 11-1: Linux 文件权限的八进制表示
| 八进制 | 二进制 | 权限 |
|---|---|---|
| 0 | 000 | --- |
| 1 | 001 | --x |
| 2 | 010 | -w- |
| 3 | 011 | -wx |
| 4 | 100 | r-- |
| 5 | 101 | r-x |
| 6 | 110 | rw- |
| 7 | 111 | rwx |
理解文件权限是直观的,但目录的行为略有不同。在目录中,读权限使得可以列举目录内的项目。写权限授予添加、删除或重命名目录项的权限。执行权限允许用户进入目录,例如使用 cd 命令,访问文件或子目录。
Linux 用户和组管理
在 Linux 中,有几个工具用于管理用户和组。最基本的命令是所有发行版都可用的,甚至在嵌入式系统上也可以使用,具体如下:
| useradd | 创建一个新用户 |
|---|---|
| usermod | 修改现有用户的属性 |
| userdel | 删除一个用户 |
| groupadd | 创建一个新组 |
| groupmod | 修改现有组的属性 |
| groupdel | 删除一个组 |
在进行安全设备工程时,最重要的命令是 useradd 和 groupadd,因为它们可以在创建镜像时用于实现与访问控制概念相关的用户和角色。
对于用户创建,当然需要提供新用户的名字。你可能还需要指定的其他选项包括其用户标识符(或UID)(--uid)、其主目录(--home)以及是否应该自动创建该目录(--create-home)。此外,你可以定义是否应该创建一个与用户名相同的用户组(--user-group)以及用户应该属于哪些组(--groups)。--shell选项允许指定登录后的 shell,但它也可以与false和nologin一起使用,以禁用用户登录。最后,你可以通过--password选项设置用户的密码(以哈希形式)。
创建组时,groupadd命令需要指定的主要参数是组名,如果需要,还可以指定一个相应的组标识符(GID)(--gid),以便稍后引用该特定组。
用户和组可能被标记为属于系统(--system),与外部的、可能是人类用户相对。此类系统账户从保留的系统范围获得 UID/GID。此外,系统用户不会过期,也不会创建主目录。
注意
这些工具 useradd 和 adduser 以及 groupadd 和 addgroup 容易混淆。本节中介绍的工具是基本的、可移植的版本,而其他工具可能在交互式会话中更易于使用。
Linux 权限管理
在 Linux 系统中,配置文件和目录的所有者、组和权限需要使用三种常见工具:
chown 允许你将文件或目录的所有者更改为另一个所有者。例如,可以使用chown bob web.conf将web.conf文件的所有者设置为bob。
chgrp 指定给定文件或目录的新组。例如,manuals目录的组可能会通过chgrp guest manuals命令更改为guest。
chmod 操作文件和目录的权限。调用chmod +x script.sh为script.sh文件添加执行权限,而chmod -wx script.sh则移除写入和执行权限。两者只会影响所有者的权限。通过在权限字符串前加上g(组)、o(其他人)、u(用户/所有者)和a(所有人),你还可以指定应该影响权限字符串的哪一部分。例如,chmod go-rwx private.key会移除所有组成员以及除了所有者之外的任何其他人的private.key文件的所有访问权限。
访问控制列表
原则上,用户和组使我们能够表示任何所需的访问控制设置。然而,在某些情况下,文件或目录只能有一个所有者和一个关联组的限制使得高效的访问控制管理变得困难。
让我举个例子,因为它的用处可能不太明显。假设你有一个名为 internal_logs 的目录,用来存储日志和运行时数据。这个目录中的文件由五个用户创建,所有用户都属于 service 组。两年后,你发布了一项新的预测性维护功能,并引入了一个名为 predmain 的新用户,该用户只需要对 internal_logs/freqtrack.dat 文件具有读取权限,并且该用户不应有写入权限——以防止在遭到入侵时造成损害。你不能将 predmain 添加到 service 组中,因为那样它将拥有过多权限,你也不能将 predmain 设置为 freqtrack.dat 的所有者,因为如果遭到入侵,攻击者将会拥有过多控制权限。
一种解决方案是使用基于 Linux 文件系统扩展文件属性(xattr)实现的访问控制列表(ACLs)。根据你的系统,必须先安装 ACL 支持,并且在挂载文件系统时需要使用 acl 选项,才能使用 getfacl 和 setfacl 命令行工具,分别查看和更改细粒度的权限。
案例研究:STM32MP157F-DK2 固件的访问控制
在本案例研究中,我首先展示了在 Yocto 中的用户和文件初始化过程。接下来,我探讨了 Linux 对某些系统文件设置的默认权限以及背后的理由。最后,作为应用层访问控制的一个例子,我查看了 SSH 守护进程 Dropbear 的配置。
Yocto 中的用户创建和文件配置
关于访问控制的第一个问题是,在任何 Linux 平台上,你必须明确如何处理 root 用户。尤其在开发过程中,root 用户可能经常被使用,甚至没有密码。确保在生产镜像中删除调试设置。在我的案例中,我从 ST 的 st-image-core 镜像中移除了 debug-tweaks 特性,如 列表 11-2 所示。
EXTRA_IMAGE_FEATURES:remove = "debug-tweaks"
inherit extrausers
ROOT_PASSWORD_HASH = "\$6\$ZsFPzdUpnha4s1lG\$8Zxzo4UhZBomryn/SJSlVq97TLy..."
EXTRA_USERS_PARAMS:append = "usermod --password '${ROOT_PASSWORD_HASH}' root;"
列表 11-2:为生产准备 root 用户
我继承了 extrausers 类,它允许修改现有的 root 用户(例如,用强密码保护它)。ROOT_PASSWORD_HASH 后面的看似神秘的字符串是 Linux 预期的用户密码哈希格式。它是通过调用 openssl passwd -6 命令获得的,其中 -6 参数表示使用基于 SHA-512 的加盐哈希。另外需要注意的是,$ 符号在这种格式中作为分隔符,并且在 Yocto 配方中需要转义。
注意
在许多情况下,完全禁用 root 登录是有意义的,除非你有令人信服的理由不这样做,以防止滥用这个强大的用户权限。
Yocto 还提供了 useradd 类,以便在自定义配方中进一步配置用户和组。列表 11-3 显示了创建两个系统用户 rservice 和 lservice,以及最终用户 admin 和 guest,同时还创建了两个相应的组,并将用户添加到这些组中。
inherit useradd
USERADD_PACKAGES = "${PN}"
USERADD_PARAM:${PN} = "--password '\$6\$fu47IexZgSH/T6d0\$9a.LjAl0sL0K...' \
--home /home/rservice --system rservice; \
--password '\$6\$tSsINjOvlFOaVrky\$9VIgdb7.LIVG...' \
--home /home/lservice --system lservice; \
--password '\$6\$VOPFagOJM.H.ZWIh\$8lELUZpkIogC...' \
--uid 1300 --home /home/admin admin; \
--password '\$6\$CPaAzKAYqkSKW42x\$KgivNUKDqsJT...' \
--uid 1301 --home /home/guest guest"
GROUPADD_PARAM:${PN} = "--system service; \
--gid 890 endusers"
GROUPMEMS_PARAM:${PN} = "--add rservice --group service; \
--add lservice --group service; \
--add admin --group endusers; \
--add guest --group endusers"
列表 11-3:为镜像创建用户和组
所有用户都使用 USERADD_PARAM 变量初始化密码。特定的 UID 仅用于最终用户账户。GROUPADD_PARAM 变量用于创建新组,而 GROUPMEMS_PARAM 则将创建的用户添加到这些组中。
在某些情况下,你可能还希望为用户创建目录并将初始文件放入其中。在列表 11-4 中,展示了一个来自自定义配方的代码片段,作为创建用户时文件配置的简单示例,包括设置所有者和组的必要命令。
do_install () {
install -d -m 770 ${D}/home/rservice
install -d -m 740 ${D}/home/lservice
install -d -m 500 ${D}/home/admin
install -d -m 550 ${D}/home/guest
install -p -m 400 administration.md ${D}/home/admin/
install -p -m 440 README ${D}/home/guest/
chown -R rservice ${D}/home/rservice
chown -R lservice ${D}/home/lservice
chown -R admin ${D}/home/admin
chown -R guest ${D}/home/guest
chgrp -R service ${D}/home/rservice
chgrp -R service ${D}/home/lservice
chgrp -R endusers ${D}/home/admin
chgrp -R endusers ${D}/home/guest
}
列表 11-4:为创建的用户提供基本文件配置
首先,为所有用户创建主目录并设置相应的权限。例如,最终用户应该只能读取提供的数据,而不能在设备上存储自己的代码或执行它。此外,管理员信息不应对 guest 用户可见。
服务用户则拥有更高的权限。它们可以将自定义数据加载到自己的目录中,而 lservice 本地服务用户拥有最高权限,因为它甚至可以读取和写入远程服务用户的目录。
通过这些基本步骤,你可以为设备的访问控制管理打下基础。
系统文件和预定义用户的探索
幸运的是,Linux 及其发行版已经为各种系统文件的权限设置做好了配置。我们来看看我的 STM32MP157F 设备固件中的一些具体示例。
Linux 上的用户和密码管理由 /etc/passwd 和 /etc/shadow 文件实现。如列表 11-5 所示,第一个文件被标记为对所有人可读,因为可能有各种合理的理由需要读取系统上的用户列表。然而,每个用户的实际密码哈希并不包含在 passwd 文件中。它存储在 shadow 文件中,该文件仅根用户可读,用于登录验证目的。
# ls -l /etc/passwd /etc/shadow
-rw-r--r-- 1 root root 1404 ... /etc/passwd
-r-------- 1 root root 884 ... /etc/shadow
列表 11-5:Linux 上密码文件的访问权限
影像密码存储已经存在数十年。其主要思想是限制非特权用户对密码哈希的访问,因为如果用户使用了弱密码,攻击者如果能够访问相应的密码哈希,就能对其进行暴力破解攻击。
注意
如果你想知道如果 /etc/shadow 只有根用户可读,甚至连 root 都不能修改密码,答案是:超级用户 root 具有类似 Chuck Norris 的能力;它甚至可以写入只读文件。
列表 11-6 展示了我设备的 microSD 卡(/dev/mmcblk0)、Linux RNG 设备(/dev/urandom)和 STM32MP157F 的硬件 RNG 设备(/dev/hwrng)的权限字符串。
# ls -l /dev/mmcblk0
brw-rw---- 1 root disk 179, 0 Apr 28 17:42 /dev/mmcblk0 # ls -l /dev/urandom /dev/hwrng
crw------- 1 root root 10, 183 Apr 28 17:42 /dev/hwrng
crw-rw-rw- 1 root root 1, 9 Apr 28 17:42 /dev/urandom
列表 11-6:microSD 卡和 RNG 设备的权限
你可以看到,对于设备文件,权限字符串指示它是块设备(b)还是字符设备(c)。结果还显示,microSD 卡只能被 root 或 disk 组的成员读取。对于 RNG,系统区分了操作系统提供的 RNG urandom,它可以被所有人读取和写入,以及硬件 RNG 设备 hwrng,只有 root 可以访问。
让我们将注意力从文件转向进程。列表 11-7 展示了典型的应用程序,如 Web 服务器 httpd 或 MQTT 经纪人 mosquitto,以及执行这些进程的相应用户。
# ps | grep -E 'PID|httpd|mosquitto'
PID USER VSZ STAT COMMAND
1138 root 5680 S /usr/sbin/httpd -DFOREGROUND -D SSL -D PHP5 -k start
1148 daemon 224m S /usr/sbin/httpd -DFOREGROUND -D SSL -D PHP5 -k start
1149 daemon 224m S /usr/sbin/httpd -DFOREGROUND -D SSL -D PHP5 -k start
1150 daemon 224m S /usr/sbin/httpd -DFOREGROUND -D SSL -D PHP5 -k start
1235 mosquitt 6792 S /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
2507 daemon 224m S /usr/sbin/httpd -DFOREGROUND -D SSL -D PHP5 -k start
2610 root 2320 S grep -E PID|httpd|mosquitto
列表 11-7:典型网络守护进程的用户上下文
Web 服务器 httpd 展示了一种常见的策略,以限制在漏洞可能被远程利用时的攻击面。它以 root 用户启动,绑定到指定端口(例如,80),然后通过调用 setgid() 和 setuid() 分别改变其 GID 和 UID,从而故意降低其高权限。因此,在我的例子中,httpd 的四个“工作线程”在低权限用户 daemon 下运行。
对于 Mosquitto 经纪人也是如此。你可以从其文档中推断出,即使以 root 用户身份启动,Mosquitto 在读取其配置文件后立即降低权限,并继续以更有限的用户(在我的例子中是 mosquitto)的上下文运行。
ps 输出的用户名限制为八个字符。因此,mosquitto 显示为 mosquitt。
SSH 守护进程访问控制配置
Dropbear 是一个轻量级的 SSH 守护进程,尤其在嵌入式系统中非常流行。它使设备能够进行安全的远程访问,这使它非常有用但也至关重要。像这样的应用程序在访问控制设置方面需要特别的关注,因为如果它们实施了“开放门政策”,那简直是在邀请攻击者。
列表 11-8 展示了从访问控制角度来看,dropbear 守护进程的一部分命令行参数。
# dropbear --help
...
-w Disallow root logins
-G Restrict logins to members of specified group
-s Disable password logins
-g Disable password logins for root
-B Allow blank password logins
-T Maximum authentication tries (default 10)
...
列表 11-8:一些 dropbear 的访问控制选项
禁用通过 SSH 的 root 访问(-w)在大多数情况下是个好主意。对于本案例研究,也可能合理限制 SSH 访问仅限于service组的用户(-G),因为这不应该是最终用户的功能。完全禁用密码登录并只允许公钥认证是完美的,但如果你的 PKI 还没有为此步骤做准备,那么-s选项就无法使用。由于我们已经完全禁用了 root 访问,使用-g就显得多余。-B参数只应在开发过程中使用;你可能不希望在生产固件镜像中看到这个。最后,你可以限制最大登录尝试次数——例如,限制为三次(-T 3)。
注意
你还可以通过-p 选项将 dropbear SSH 守护进程的端口从 22 更改为自定义端口。这只是为了增加隐蔽性,但它可能会帮助你的联网设备免于被自动 SSH 扫描发现。
要持久存储你的dropbear设置,你必须更改固件镜像中的/etc/dropbear/default文件。要实现上述访问控制限制,重要的一行内容是DROPBEAR_EXTRA_ARGS="-w -T 3 -G service"。
强制访问控制
尽管 DAC 概念在嵌入式系统开发人员中通常是已知的,强制访问控制(MAC)往往是未知的领域。然而,MAC 实现可以显著提升设备的安全性,并在设备遭到破坏时限制损害。
嵌入式设备的 MAC 系统的基本思想是,关于用户和进程如何与文件和其他资源进行交互的权限和策略由制造商管理,并由操作系统强制执行。与 DAC 以用户为中心的方式不同,用户无法覆盖 MAC 定义的规则。
MAC 实现是强大的工具,但权力伴随着责任。白名单是一种流行的访问控制策略,默认情况下拒绝访问,只有显式允许时才授予访问权限。此方法也可以用于 MAC 系统,允许定义的主体对对象的访问。然而,如果你省略了指定某个合法访问为“允许”,也许是因为它很少被使用,那么在访问发生时,要求此访问的应用程序可能会在运行时崩溃。
如果你选择黑名单方法——只定义需要拒绝访问的危险情况,比如病毒扫描器检测到的恶意软件——则破坏功能的概率会降低。然而,你必须确保及时将新发现的恶意行为的相应规则添加到设备中。
Linux 安全模块
由于 Linux 社区未能就一个特定的安全模块达成一致,Linux 引入了Linux 安全模块(LSM)框架。它使得在 Linux 上实现多种 MAC 系统成为可能。
这些 LSM 被编译到 Linux 内核中,并在内核代码中调用特定的钩子函数时采取相应的行动。这些钩子集成在操作系统中所有与访问控制相关的过程之中,从文件访问到任务生成,再到进程间通信。如果到达钩子,内核将控制权交给 LSM,LSM 至少可以记录执行的操作,或者根据其特定规则集直接决定是否允许访问。
不同的 LSM 实现方式在概念、配置规则集的方式以及支持社区方面有显著差异。然而,它们也有一个共同点:都对系统性能产生负面影响。接下来的章节将介绍一些流行的 LSM 实现。
SELinux
2000 年,NSA 将其针对 Linux 的 MAC 系统理念发布给开源社区:安全增强 Linux(SELinux)。在该领域其他利益相关者的支持下,该项目蓬勃发展,并最终在 2003 年集成到主线 Linux 内核的 2.6 版本中。从版本 4.3 开始,它成为 Android 的默认 LSM,许多桌面和服务器应用的 Linux 发行版也都支持它。
SELinux 依赖于定义哪些对象可以被哪些主体访问的安全策略。为此,必须在 SELinux 中注册对象和主体,并为其分配相应的标签,标签中包含用户、角色和关联类型。这些标签定义了主体和对象的某种上下文或领域。实际的访问控制是通过类型强制实现的,类型强制定义了具有特定类型的主体是否可以访问具有特定类型的对象。
许多 Linux 发行版提供了自己预定义的 SELinux 策略集,以限制各种常见的应用和服务。此外,还有一个参考策略数据库,您可以根据需求使用。然而,为您的应用创建自定义策略需要深入了解其功能,并对 SELinux 的概念和结构有详细的理解。即便有工具可以支持您,也不应低估所需的特征化工作量。
在运行时,SELinux 可以以三种方式操作。强制模式适用于生产环境,因为它严格应用所有给定的策略并记录相应活动。然而,在开发或测试阶段,宽容模式更为适用。它处理所有策略,但仅生成警告和日志数据,而不强制执行定义的规则。这对于微调自定义策略和故障排除非常有帮助。如果禁用,SELinux 将完全关闭,不会保护或限制任何内容。
尽管(或者也许正因为)SELinux 提供了大量的能力,它仍然是一个相当复杂的工具,许多嵌入式系统工程师因此不愿使用它。这可能是其他 LSM 实现出现并成为流行替代方案的主要原因,如接下来的部分所述。
AppArmor
AppArmor是第二个在 Linux 发行版中获得显著普及的 LSM 实现。它于 2010 年成为 Linux 内核 2.6.36 的一部分,目前是 Ubuntu 和 SUSE Linux 的默认 MAC 系统。自 2009 年以来,它的开发得到了 Canonical 的资助。
访问控制是基于每个应用程序的个人配置文件进行管理的。与 SELinux 不同,AppArmor 使用文件系统路径来识别主体和文件对象,因此其语法具有更好的可读性。此外,它还支持混合的白名单和黑名单规则方法,控制进程的资源访问。创建的配置文件可以限制网络访问和各种 Linux 能力,还可以限制读取、写入和执行文件的权限。
AppArmor 提供了一些预定义的配置文件,Ubuntu 社区还维护了常见应用程序的额外配置文件。此外,AppArmor 还提供了多个工具,帮助开发者为自定义应用程序配置文件并生成相应的配置文件。
一般来说,有两种方式来描述访问需求。首先,目标化的配置文件方法允许你捕获单个应用程序的访问事件,并从中自动生成配置文件。其次,AppArmor 可以应用系统监控方法,记录一组定义应用程序的访问操作,持续数天甚至数周,并跨多个重启。收集的日志信息可以转化为一系列配置文件,以尽可能优化的方式限制分析过的应用程序。
设备上的每个 AppArmor 配置文件在运行时可以处于三种模式之一:强制、抱怨或审计。在强制模式下,配置文件设定的规则会被强制执行,任何违规尝试都会被记录。抱怨模式允许监控在定义的配置文件下应用程序的行为,违规操作会被记录。这个模式也用于前面提到的自动化配置文件创建,因此有时被称为 AppArmor 的学习模式。为了在强制执行给定策略的同时记录所有访问,无论是否成功,必须选择审计模式。
AppArmor 是一个值得推荐的 SELinux 替代方案,成功地降低了配置和配置文件的复杂性。从安全性角度来看,它有时比竞争对手更加宽松,某些情况下可能存在绕过访问控制的空间。然而,对于嵌入式系统来说,它可能是引入 MAC 机制的完美折衷方案。
其他 LSM 和非 LSM MACs
除了两个流行的 LSM 实现,SELinux 和 AppArmor,你还可以考虑其他选项。
正如其名称所示,简化强制访问控制内核(SMACK)系统的开发重点是简化,与 SELinux 的复杂性相对。自 2008 年起,它一直是 Linux 主线内核的一部分,并始终旨在用于嵌入式系统。两个较大的操作系统项目依赖于其保护机制:用于三星智能电视的移动操作系统 Tizen,以及旨在为联网汽车提供开源平台的汽车级 Linux 发行版。然而,查看 SMACK 的官方网站及其 Git 仓库,似乎它不再积极维护。
基于 LSM 框架的另一个 MAC 系统被称为TOMOYO。该项目始于 2003 年,并在 2009 年合并进 Linux 内核 2.6.30。其动机是为了简化使用和提高可用性——例如,通过自动生成策略来实现,这也是必要的,因为这个 MAC 系统并没有为常见服务提供一套全面的规则。此外,TOMOYO 不仅作为 MAC 实现,还促进了系统行为分析。它有三个版本:1.x、2.x和 AKARI。第一个版本需要特定的内核补丁,因此通常不是首选。AKARI 和 TOMOYO 2.x使用 LSM 框架。在撰写本文时,AKARI 提供了一些额外的功能,但 TOMOYO 2.x正在赶超。
虽然 LSM 框架为集成自定义安全模块提供了多种可能性,但并非所有社区成员都对其实现感到满意,尤其是考虑到它带来的性能开销。因此,也存在非 LSM 的 MAC 系统,旨在提高性能或增强安全模块功能。然而,由于这些实现不是主线内核的一部分,必须通过应用自定义补丁集来集成,它们可能只有在你无法通过流行的 LSM 实现满足需求时才是一个可行的选择。
案例研究:使用 AppArmor 进行应用程序限制
在本案例研究中,我将重点介绍如何在我的 STM32MP157F-DK2 设备上使用 Yocto 工具链安装 AppArmor,并演示如何通过基本使用来限制应用程序。
安装
AppArmor 不包含在 ST 的 OpenSTLinux 发行版的默认安装中。幸运的是,Armin Kuster 维护的 Yocto meta-security层提供了一个位于meta-security/recipes-mac/AppArmor下的 AppArmor 食谱。
克隆相应的 Git 仓库后,可以通过清单 11-9 中所示的设置,配置 Linux 内核以使用 AppArmor。
CONFIG_SECURITY=y
CONFIG_SECURITY_APPARMOR=y
CONFIG_DEFAULT_SECURITY="apparmor"
CONFIG_SECURITY_APPARMOR_BOOTPARAM_VALUE=1
清单 11-9:启用 AppArmor 的 Linux 内核配置
前两行启用 AppArmor,最后两行则将其设置为默认使用的 LSM。然而,我还需要在 U-Boot 的extlinux.conf文件中添加security=apparmor,以在启动时选择 AppArmor。
要编译和安装 AppArmor 用户空间工具,请在镜像的配方中添加 IMAGE_INSTALL += "apparmor" 这一行。我还必须向提供的 OpenSTLinux 添加几个发行版特性,如 列表 11-10 所示,以便使 Yocto 成功完成构建过程。
DISTRO_FEATURES += "security"
DISTRO_FEATURES += "apparmor"
DISTRO_FEATURES += "tpm"
列表 11-10:来自 meta-security 的 AppArmor 发行版特性
启动设备后,您可以使用 列表 11-11 中显示的命令检查 AppArmor 是否已正确启用。如果返回 Y,则表示已正确激活。
# cat /sys/module/apparmor/parameters/enabled
Y
列表 11-11:检查是否正确启用了 AppArmor
AppArmor 附带了 aa-status 工具,它列出了有关 AppArmor 当前状态的各种详细信息,如 列表 11-12 所示。
# aa-status
apparmor module is loaded.
50 profiles are loaded.
50 profiles are in enforce mode.
...
apache2
...
avahi-daemon
...
ping
...
syslogd
traceroute
...
0 profiles are in complain mode.
...
2 processes are in enforce mode.
/usr/sbin/avahi-daemon (665) avahi-daemon
/usr/sbin/avahi-daemon (667) avahi-daemon
0 processes are in complain mode.
...
列表 11-12:初始的 aa-status 输出
您可以看到来自 meta-security 层的 AppArmor 配方也安装了一组 50 个标准配置文件,这些配置文件在我的设备上以强制模式加载。然而,我首先注意到的是,尽管为 apache2 和 syslogd 加载了配置文件,但相应的当前运行的进程并未受到限制。只有 avahi-daemon 进程根据其配置文件受到限制。
要调查此问题,我们需要查看存储在 /etc/apparmor.d/ 中的默认 AppArmor 配置文件。对于 apache2,包含提供的配置文件的文件名为 usr.sbin.apache2。文件名已经暗示了它所限制的可执行文件路径:/usr/sbin/apache2。查看文件内容,可以看到一行 profile apache2 /usr/\{bin, sbin\}/apache2,这意味着当前的配置文件名为 apache2,并且目标是位于 /usr/bin/ 或 /usr/sbin/ 的可执行文件 apache2。
不幸的是,在我的安装中没有这个文件。相反,它被命名为 httpd。因此,我创建了一个名为 usr.sbin.httpd 的初始文件副本。我还将配置文件名称更改为 httpd,并将可执行文件的路径更改为 /usr/\{bin,sbin\}/httpd。然后,我按 列表 11-13 中所示的方式加载了该配置文件,并重新启动了 Web 服务器。
# aa-enforce /etc/apparmor.d/usr.sbin.httpd
Setting /etc/apparmor.d/usr.sbin.httpd to enforce mode.
# systemctl restart apache2
# aa-disable /etc/apparmor.d/usr.sbin.apache2
Disabling /etc/apparmor.d/usr.sbin.apache2.
列表 11-13:加载和禁用配置文件
我还通过 aa-disable 禁用了原始配置文件,以便清理。
列表 11-14 展示了另一次调用 aa-status 的输出,显示 httpd 配置文件已正确加载,并且所有四个相应的进程实例都按预期在强制模式下运行。
# aa-status
...
50 profiles are loaded.
50 profiles are in enforce mode.
...
httpd
httpd//DEFAULT_URI
httpd//HANDLING_UNTRUSTED_INPUT
httpd//phpsysinfo
...
6 processes are in enforce mode.
/usr/sbin/avahi-daemon (669) avahi-daemon
/usr/sbin/avahi-daemon (672) avahi-daemon
/usr/sbin/httpd (668) httpd
/usr/sbin/httpd (678) httpd
/usr/sbin/httpd (679) httpd
/usr/sbin/httpd (681) httpd
...
列表 11-14:更改配置文件后的 aa-status 输出
尽管我们成功激活了预定义的配置文件,但我们不知道该配置文件是否以安全的方式实际限制了 Web 服务器应用程序。给定配置文件中的一条注释说:“这个配置文件完全是宽松的”,这意味着您仍然需要根据您的应用程序和需求对其进行定制。
快速查看与syslogd工具关联的 sbin.syslogd 配置文件,这个工具在本小节开头被标识为第二个示例二进制文件,可以发现配置的路径 /sbin/syslogd 与相应可执行文件的路径一致,但进程仍然没有以强制模式运行。如清单 11-15 所示,二进制文件的属性显示,该可执行文件实际上是指向另一个可执行文件的符号链接——即 /bin/busybox.nosuid。
# ls -l /sbin/syslogd
lrwxrwxrwx 1 root root 19 ... /sbin/syslogd -> /bin/busybox.nosuid
清单 11-15:指向另一个可执行文件的符号链接
这种情况有些复杂,因为 BusyBox 将多种工具合并在一个二进制文件中。仅仅更改 syslogd 配置文件的路径并不能解决这个问题,反而会导致其他 BusyBox 功能出现问题。在这种情况下,你有几个选择。你可以只放宽 syslogd 配置文件,也可以搜索或创建一个全面的 busybox 配置文件,或者你也可以最终安装并使用原版 syslogd 应用程序。
应用程序剖析
对于你自己的应用程序或没有预定义 AppArmor 配置文件的第三方工具,如果你希望在运行时通过 MAC 机制限制它们,你必须自己创建一个配置文件。
我们来看一个被简化到最小的 Python 应用。清单 11-16 展示了代码。
#!/usr/bin/python3
import sys
if len(sys.argv) == 2:
file_path = sys.argv[1]
with open(file_path, 'r') as f:
print(f.read())
else:
print('Usage:', sys.argv[0], '<filename>')
清单 11-16:一个简单的 Python 打印文件应用
这个应用程序的唯一目的是打印作为命令行参数给定的文本文件的内容。应用程序的文件名是 printfile.py,位于 /home/root/ 目录下,并且被标记为可执行。
假设这个工具是你 web 界面的重要组成部分,并且需要超级用户权限运行,因为它必须打印 testfile 和 logfile 文件的内容,这些文件只能由 root 访问。然而,在进行威胁和风险分析时,你发现攻击者可能能够注入除了两个预定文件路径之外的其他路径,这可能会导致敏感信息泄露,必须加以防范——例如,通过使用量身定制的 AppArmor 配置文件。
清单 11-17 展示了我在 /etc/apparmor.d/home.root.printfile.py 中创建的基本初始配置文件,作为配置此应用程序的起点。它包括对之前提到的两个文件的读取权限(r),并拒绝任何其他文件访问。
/home/root/printfile.py {
/home/root/testfile r,
/home/root/logfile r,
}
清单 11-17:printfile.py 的初始 AppArmor 配置文件
在第二步中,我以告警模式加载了新创建的配置文件,如清单 11-18 所示。
# aa-complain /etc/apparmor.d/home.root.printfile.py
Setting /etc/apparmor.d/home.root.printfile.py to complain mode.
清单 11-18:以告警模式加载配置文件
如果你现在在 home/root/ 中执行 ./printfile.py testfile,应用程序将正常运行,但会为所有配置文件违规情况创建日志条目。
清单 11-19 展示了与 printfile.py 相关的精简版 AppArmor 内核消息。
# dmesg | grep printfile.py
... audit: type=1400 audit(1652997509.490:132): apparmor="STATUS"
operation="profile_load" profile="unconfined"
name="/home/root/printfile.py" pid=1557 comm="apparmor_parser"
... audit: type=1400 audit(1652997771.210:133): apparmor="ALLOWED"
operation="open" profile="/home/root/printfile.py"
name="/etc/ld.so.cache" pid=1560 comm="printfile.py"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0
... audit: type=1300 audit(1652997771.210:133): arch=40000028 ...
comm="printfile.py" exe="/usr/bin/python3.10"
subj=/home/root/printfile.py (complain) key=(null)
... audit: type=1400 audit(1652997771.210:134): apparmor="ALLOWED"
operation="open" profile="/home/root/printfile.py"
name="/usr/lib/libpython3.10.so.1.0" pid=1560 comm="printfile.py"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0
... audit: type=1300 audit(1652997771.210:134): arch=40000028 ...
comm="printfile.py" exe="/usr/bin/python3.10"
subj=/home/root/printfile.py (complain) key=(null)
... audit: type=1400 audit(1652997771.210:135): apparmor="ALLOWED"
operation="file_mmap" profile="/home/root/printfile.py"
name="/usr/lib/libpython3.10.so.1.0" pid=1560 comm="printfile.py"
requested_mask="rm" denied_mask="rm" fsuid=0 ouid=0
... audit: type=1300 audit(1652997771.210:135): arch=40000028 ...
comm="printfile.py" exe="/usr/bin/python3.10"
subj=/home/root/printfile.py (complain) key=(null)
... audit: type=1400 audit(1652997771.210:136): apparmor="ALLOWED"
operation="open" profile="/home/root/printfile.py"
name="/lib/libc.so.6" pid=1560 comm="printfile.py"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0
Listing 11-19: printfile.py 的 AppArmor 抱怨消息
你可以看到,在以预期的方式调用printfile.py时发生了多个访问违规。如果你将初始配置文件放入强制模式,应用程序将不再工作。因此,你需要使用所示输出扩展printfile.py的 AppArmor 配置文件。例如,你需要授予/etc/ld.so.cache的读取权限(r),/usr/bin/python3.10的执行权限(ux),以及读取(r)和映射(m)/usr/lib/libpython3.10.so.1.0的权限。
Listing 11-20 展示了在抱怨模式下执行四次、配置文件优化和重新加载后的最终配置文件。
/home/root/printfile.py flags=(complain) {
/home/root/testfile r,
/home/root/logfile r,
/etc/ld.so.cache r,
/usr/bin/python3.10 ux,
/usr/lib/libpython3.10.so.1.0 rm,
/lib/libc.so.6 rm,
/lib/libm.so.6 rm,
/usr/lib/locale/locale-archive r,
/usr/lib/python3.10/ r,
/usr/lib/python3.10/** r,
/home/root/printfile.py r,
}
Listing 11-20:优化后的 printfile.py 的 AppArmor 配置文件
在这个手动表征阶段之后,创建的配置文件可以在强制模式下加载,并测试其行为(Listing 11-21)。
# aa-enforce /etc/apparmor.d/home.root.printfile.py
Setting /etc/apparmor.d/home.root.printfile.py to enforce mode.
# ./printfile.py testfile
--- --- ---
This is a test file!
--- --- ---
# ./printfile.py logfile
--- --- ---
All the logs...
--- --- ---
# ./printfile.py secrets
Traceback (most recent call last):
File "/home/root/./printfile.py", line 7, in <module>
with open(file_path, 'r') as f:
PermissionError: [Errno 13] Permission denied: 'secrets'
# ./printfile.py /etc/passwd
Traceback (most recent call last):
File "/home/root/./printfile.py", line 7, in <module>
with open(file_path, 'r') as f:
PermissionError: [Errno 13] Permission denied: '/etc/passwd'
Listing 11-21:在强制模式下测试 printfile.py *
打印testfile和logfile文件按预期工作。然而,如果攻击者尝试读取同一文件夹中的secrets文件,甚至是/etc/passwd文件,AppArmor 将成功防止严重损害。
这个简单的案例研究展示了应用程序表征及相应的 AppArmor 配置文件创建的基本可行性。然而,即使是这个简单的示例也经历了多个配置文件迭代,且生成的配置文件将需要持续维护——例如,如果你切换到另一个发行版或甚至更新版本的 Python。此外,正如你可以想象的,复杂的应用程序需要显著更多的表征和测试工作。
总结
访问控制是一个极其广泛的话题,可以单独写成一本书。它涵盖了用户、组、目录结构和访问权限的基本配置(本章讨论了 Linux DAC 系统),禁用不适用于终端用户的硬件调试功能和工具,以及必须根据特定应用程序的行为和资源访问需求进行微调的操作系统强制 MAC 策略的复杂领域。尽管我不指望你把所有时间都用来设计完美的访问控制设置,但在安全设备工程中,绕过这个问题是不可行的。
实际操作中,我们必须找到切实可行的折中方案。完全的白名单策略可能难以实施,并且如果配置错误,可能会导致应用程序崩溃,从而受到安全指责。另一方面,黑名单策略则无法立即捕捉到出现的新威胁。权限的精细粒度只能通过时间管理,如果你愿意付出大量努力,但如果你的访问控制概念过于简单,敌人将会感谢你移除了那个恼人的障碍。
最后,我想指出,访问控制机制总是与系统完整性保护有着密切关系,如第八章所述。试想一下,如果你辛苦定义了一套完美的访问规则,却发现攻击者可以在几分钟内将它们全部重置为 777,那将非常痛苦。
第十二章:系统监控**

监控常常带有负面含义,因为它暗示数据是被某人或某物用于监视。然而,许多数字商业模式,如预测性维护,如果没有监控和随后的数据分析,将无法正常运作。同样,设备的持续保护也需要数据日志记录和分析,以便检测和追踪恶意活动。
在 IT 系统中创建大量日志文件已经是几十年来的常见做法。然而,当谈到嵌入式系统时,日志可能没有得到应有的重视。在过去,存储容量低和缺乏互联网连接是将系统监控放在次要位置的合理理由,但幸运的是,如今这些限制已经逐渐减少。
在本章中,我将解释出于安全原因监控设备的好处。此外,我还将讨论嵌入式系统的资源限制,以及日志处理过程的多个方面及其管理。最后,本书的案例研究将探讨如何分析 STM32MP157F 设备上的可用日志数据。
出于正确的理由进行监控
许多公司被指控为“数据克拉肯”,因为它们收集所有能够获取的数据,无论是来自网络服务、IT 基础设施,还是终端用户设备。对于这些公司而言,数据收集带来了盈利的商业模式,但这一发展显然与隐私保护的努力相冲突,隐私保护旨在防止人们被跟踪、监视和间谍行为。
在法律方面,欧洲的 GDPR 就是一种反对过度数据收集的例子,但其他法律要求——例如汽车行业的联合国 155 号规定、工业自动化行业的 IEC 62443-4-2 标准,以及北美电力可靠性公司(NERC)发布的《关键基础设施保护标准》(CIP-007-6)——明确要求对连接产品进行安全监控,且这些监控要由制造商或运营商负责。简而言之,务必意识到数据收集和监控的两面性必须始终保持谨慎平衡。
在某些领域,监控已经是常见的做法;例如,监控摄像头和安保人员时刻关注着建筑物和公司场所的物理安全威胁。银行业在监控交易并对异常活动作出反应方面有着悠久的传统。最后但同样重要的是,防火墙和入侵检测系统对网络流量的监控是每个理智的 IT 基础设施的一部分。出于安全目的收集数据和监控系统显然是可以接受的,但必须始终保持在合理范围内。
系统监控对设备安全有多方面的好处。日志数据在两个方面尤其有用。首先,对设备的每一次攻击都经历多个阶段,包括侦察、投递、利用、安装、指挥与控制、以及目标操作。每个阶段可能会在设备的日志文件中留下痕迹,从而使你能够早期检测到攻击,并作出相应反应,避免进一步损害。其次,日志数据在事件发生后的取证过程中可以是极为宝贵的资源。试想一下,你的设备在现场遭遇攻击,但没有任何数据来重建和理解发生了什么。处于这种情况中的盲目状态,不仅让你毫无头绪,还可能给你的业务带来重大风险。
总的来说,设备监控提高了你在现场设备群体中的透明度,并使你能够概览何时、何地以及如何使用你的产品。在关键基础设施中,这种监控甚至可能是履行法律或认证要求所必需的。此外,正如第九章中提到的,了解你在现场设备上部署的固件版本的状态,有助于理解客户的威胁态势。
除了纯粹的安全使用案例外,监控甚至可以作为处理和分析客户投诉的辅助工具。例如,日志数据可能证明最终用户进行了手动配置更改,导致设备进入未经授权的操作模式,超负荷运行的机械部件最终导致机器故障。
监控正确的内容
运行 Linux 的嵌入式系统,与各种服务进行通信,并与传感器和执行器进行互动,已达到一种复杂性,使得在任何给定时间点记录整个设备状态变得不现实。唯一的选择是对系统属性和事件进行选择性监控。
警告
日志系统数据是把双刃剑。它在故障排除过程中可能对服务工程师大有帮助,但如果日志中包含用户名、密码及其他敏感数据,攻击者可能会免费利用这些信息。
用户互动与访问控制
一个好的起点是监控与用户互动和访问控制相关的事件。
用户会话
如果人类登录到一个设备,他们的行为是有目的的,无论是授权的还是恶意的。了解用户的登录历史,并能将其与某个用户的实际活动相匹配,可能会揭示账户被滥用的情况。例如,与一个虚拟用户对应的人可能正在度假,因此显然不可能在进行操作。
此外,大量失败的登录尝试可能表明已经进行了暴力破解攻击。最后,用户会话的时间和日期信息可能有助于重建攻击的时间线。
访问控制违规
如第十一章所述,用户和进程应当被限制为完成任务所需的最低权限。除了运行时访问限制的积极效果之外,这些措施还可以提供关于哪个用户或进程试图访问禁止的资源的信息,这可能有助于在早期阶段发现攻击。
文件系统活动
突然出现的临时文件以及对配置或系统文件的永久性更改,可能表明存在恶意活动。它们可能由设备上运行的进程启动,可能是因为这些进程被利用了未知漏洞。但也有可能是人为用户发起了操控,这可能暗示着内部攻击。在这种情况下,至少监控一些关键文件及其事件可能非常有价值。
可移动媒体
有时,只需将 USB 闪存盘或内存卡插入设备,就可能触发攻击。如果你的产品提供此类功能,记录何时插入或移除这些存储介质的信息,甚至是它们的标识符,完全是有意义的。
通信
尽管由于传输数据量庞大,通常无法监控设备所有通信通道上的所有数据,但某些元数据或特定发送或接收的信息可以提供有价值的洞察。
网络通信
从外部来源接收到的通信数据以及设备发送的数据,可能揭示恶意活动的线索。例如,来自某些 IP 地址的大量流量可能表示定向的 DoS 攻击。非标准的通信协议数据包可能代表攻击者在测试设备对被篡改请求的反应。
从设备到未知位置和 IP 地址的新连接,或异常增加的外向网络流量,也可能表明设备已被攻破。
低级通信
与传统 IT 系统不同,嵌入式系统通常具有多种低级接口,如 I²C 总线、串行连接或通用输入输出(GPIO)引脚。它们的通信通常携带传感器或控制数据,这可能再次成为检测异常行为的来源,例如强迫执行器进入危险操作模式。
记录这些接口的状态和通信数据可能不是标准功能,但根据它们的关键性,实施这一功能可能值得考虑。
应用行为
与从用户或通信接口角度查看安全监控不同,考虑采用以应用程序为中心的方法可能更有意义。
服务活动
一些服务提供有关设备状态的有趣信息,特别是那些具有远程连接的服务。例如,允许远程会话的 SSH 守护进程可能提供带有相应 IP 地址和用户名的登录尝试历史记录。
Web 服务器还能够记录它接收到的 HTTP 请求,这可能揭示应用层通信中的敌对活动。
自定义应用程序
专有软件组件,例如与你的商业模式相关的,可能提供设备使用的深刻洞察。因此,将监控设施集成到你自己开发的重要应用程序中应该是很自然的。
第三方应用程序
每当你允许用户在设备上安装第三方开发的应用程序时,就面临着来自不可信软件组件的常见威胁。除了对这些程序进行严格的访问管理外,持续监控它们的行为也是一种合理的措施。
系统行为
最后但同样重要的是,设备的整体“健康状态”可能揭示攻击或至少是你可以进一步分析的异常行为。
系统利用率
开发人员和维护人员通常了解典型的系统利用率值,如 CPU 负载和内存消耗。如果设备显示异常高的值,可能表明设备上运行了额外的软件,例如加密矿工或攻击者安装的后门服务。但它也可能只是针对低级或网络接口的攻击的副作用。
进程和系统崩溃
你的设备进程是否偶尔崩溃?我希望不是,但也许它们只是偶尔在你测试时没有覆盖到的极少数情况下崩溃。如果单个进程甚至整个系统多次崩溃,可能是攻击者正在探测设备的潜在漏洞。如果能够尽早检测并分析出来,你可能能够采取措施,防止进一步的损害。
错误和警告
尽管错误信息和警告可能是完全正常的设备行为的一部分,但它们也可能是敌对者在侦察阶段篡改设备服务或配置选项的证据。
基于风险的监控
哪些监控措施对于你的产品是合理的,应该基于设备的威胁和风险分析来决定,如第一章中所述。例如,如果你发现高网络负载是设备实时操作的风险,监控网络通信和系统利用率是个好主意。如果设备允许运行第三方 Docker 容器,那么监控这些潜在危险软件包的活动应该在你的监控清单上。如果设备有用于配置的 Web 接口,那么记录 Web 服务器事件就成为一种必要措施。
注意
可能存在更多针对您设备的安全监控有价值的信息来源。找到它们并利用它们来保护您的设备。
设计监控方案
虽然系统监控对于 IT 系统来说并不新鲜,但高效地实现它始终是一个挑战——尤其是在嵌入式系统上记录事件和过程时。根据特定产品的功能和使用要求,必须在设备端和基于服务器的处理之间找到一个折衷方案。
图 12-1 显示了日志数据的通用数据流架构,从数据的收集、本地存储到最终的设备端分析。尽管受限的设备会将这一部分保持得较为简洁,但较强的设备可能会聚合更多数据并已在设备端进行分析,以最大化潜在的洞察。

图 12-1:通用日志架构
通常,网络设备至少会将收集到的信息的一部分传输到远程服务器,无论是在本地网络上还是在互联网上。这不仅可以存储更多的日志数据,还可以进行对单个设备以及整个设备群体的性能密集型数据分析。尤其是后者,显著提高了安全监控的质量。
嵌入式系统的挑战
当涉及到(I)IoT 场景中设备相对受限且可能存在相当大规模的人群时,监控过程的具体设计面临着几个挑战。
有限资源
嵌入式系统在多个维度上可能受到限制,这使得开发者在确定用于监控的资源量时面临困难。有些设备是电池供电的,收集(更不用说传输)日志数据会进一步增加电池的负担。对于基于小型微控制器的产品,出于性能考虑,日志数据的收集是不可能的,因为根本没有时间执行这些任务。有些系统的存储空间有限,只能存储少量的日志数据,而其他系统则没有将监控数据传输到中央服务器所需的高带宽网络连接。有些设备同时具备这些限制。
对于已经针对单一专用任务优化的现有产品而言,通常很难或几乎不可能将安全监控作为附加功能进行集成。但这不应成为放弃所有监控功能的借口。如果您能在开发生命周期的早期识别出安全监控的需求,您有机会为这一绝对相关的目的保留资源。
同步时间源
正确的时间戳对于设备内部及与其他设备和系统组件之间的事件关联至关重要。为此,时间源必须进行同步,而在嵌入式系统中这一点不能理所当然。
需要故意启用像 NTP 这样的同步协议支持,使用你产品的集成商以及这些系统的操作员必须提供适当的基础设施,如主时钟和网络中的时间服务器。
安全通信
从设备到中央收集服务器的监控数据的安全传输需要满足多个前提条件。设备必须能够验证中央服务器的身份,否则可能会被欺骗,向未经授权的方发送重要数据。
服务器也必须能够验证提供数据的设备的身份,因为虚假设备可能会通过发送篡改数据严重干扰监控过程。此外,数据传输本身应该在所有安全通信方面得到保护,具体如第七章所述。
完整性保护的本地存储
如果你决定部分或完全依赖设备端日志而不进行外部传输,你必须准备好回答这样一个问题:“你如何保证日志数据在某个时候没有被篡改?”这个问题甚至可能会由法律实体或法庭提问,例如,如果你的设备是某个国家关键基础设施的一部分。
如第八章所描述的系统完整性保护是解决这一挑战的根本要求。
保密保护的本地存储
如前所述,日志数据不仅对调试设备有用,也可能对攻击者在其侦察阶段有用。因此,你可以把日志文件视为需要保密保护的资产。
如第五章所解释的那样,像加密文件系统这样的措施可以作为一种解决方案。另一种选择是实施混合加密方案,使用中央日志服务器的公钥加密一个随机的本地对称密钥,该密钥用于加密本地生成的日志数据。在这种情况下,只有持有相应私钥的远程服务器才能解密和分析日志信息。
设备日志过程的监控
持续的安全监控在检测攻击、及时响应事件以及在妥协后进行全面的取证分析中起着关键作用,这意味着涉及的设备日志过程必须正常运行。然而,假设监控过程天生稳健并能免疫所有影响是天真的。重要的是要安装措施,检查相关的日志过程是否正常工作。
无论本地日志存储只有几兆字节还是几个千兆字节,它始终是有限的。在某些情况下,可能会诱使人说某个内存可以存储“比某个特定设备可能生成的更多的日志事件”,但请记住,日志行为可能会随着未来固件版本的更新而变化,工业场景中的设备可能会运行数十年,并且新特性和内容可能会显著减少存储容量。因此,监控日志存储容量是一个必须执行的任务。此外,您还必须决定如果存储空间耗尽,或者甚至在发生之前如何应对。例如,如果可能的话,可以将日志压缩并归档到远程位置,或者通过新产生的日志事件覆盖最旧的事件。
除了本地存储容量问题外,用于传输日志数据的远程连接也可能会受到影响。您可以轻松想象,这种通信的带宽可能会突然下降,甚至完全断开连接。在这种情况下,设备需要决定是否将收集的数据临时存储在本地,直到重新建立连接并且远程位置能够跟上,或者是否至少部分或完全丢弃监控数据。
此外,您的日志应用程序可能会碰到未知的错误,或者对手可能故意攻击您的监控系统。这两种情况都可能导致关键系统部分崩溃,并让您在监控数据方面失去视线。即使您的设备不能从所有可能的情况中恢复,崩溃后自动重启监控服务并本地记录有关最终丧失监控能力的事件仍然是值得的。
集中日志分析与管理
在许多使用案例中,拥有一个集中存储位置来监控数据,并对来自整个设备群体的信息进行分析和关联是非常有价值的。除其他优点外,这种方法使您能够在短时间内对现场的安全相关事件做出反应。如果攻击者正在篡改关键配置或系统文件,您可能希望尽快得知此事。这种方法还使您能够将设备日志数据的长期历史纳入当前分析中,并且一旦数据被传输,设备上对日志信息的篡改或破坏将变得显而易见。
然而,实施图 12-1 中显示的日志架构第二行并非易事。在(I)IoT 场景中,一个挑战可能是中央监控系统的可扩展性和相应分析能力的可用性。当然,这取决于你的设备群体的大小。虽然几千个产品可能是可管理的,但当监控六位数甚至更多设备时,可扩展性变得至关重要。这不仅需要大量的中央存储能力,还需要人员和算法来筛选所有数据,以识别意外的设备行为、出现的恶意活动或妥协指示符(IoC)。
用于此目的的系统通常被称为安全信息和事件管理(SIEM)系统,因为它们收集与安全监控相关的事件数据,并提供一种手段来找到“干草堆中的针”。这类分析平台的示例包括 Elastic Stack、Graylog 和 Splunk,仅举几例。虽然大数据分析和人工智能可以为此任务提供强有力的支持,但在许多情况下,判断和最终由人工专家进行手动调查是必需的。
如第一章所述,每个安全产品生命周期必须建立某种类型的事件和漏洞响应流程。确保操作 SIEM 系统的团队与产品开发和管理团队保持紧密联系,这将使你在事件和漏洞处理方面提高效率。
尽管中央监控系统和流程具有诸多优点,但不要忘记批判性地评估你真正需要哪些数据来支持你选择的监控方法。堆积大量不必要的数据不仅会使你的解决方案低效,还可能与欧洲 GDPR 或类似的法律发生冲突,特别是当你的数据库包含个人身份信息(PII)时。
注意
如果你不熟悉 PII 收集的法律限制,请深入了解这个话题。即便是类似 IP 和 MAC 地址这样的“技术”数据也可能追溯到自然人,因此需要特殊处理,或者根本不应收集。
案例研究:在 STM32MP157F 设备上记录事件
本案例研究展示了从像 STM32MP157F-DK2 板这样的基于 Linux 的嵌入式系统中提取监控数据的典型工具和配置。
使用 journald 进行用户会话监控
运行systemd的 Linux 系统,比如我的 STM32MP157F-DK2 固件,带有journald工具,该工具收集有关基本系统进程的有价值信息。
例如,列表 12-1 展示了一系列通过grep logind从journalctl中提取的日志条目。
# journalctl | grep logind
...
Apr 28 21:07:31 stm32mp1 systemd-logind[650]: New session c1 of user root.
Apr 28 21:13:51 stm32mp1 systemd-logind[650]: Session c1 logged out. ...
Apr 28 21:13:51 stm32mp1 systemd-logind[650]: Removed session c1.
Apr 28 22:54:57 stm32mp1 systemd-logind[650]: New session c2 of user root.
Apr 28 22:55:10 stm32mp1 systemd-logind[650]: Session c2 logged out. ...
Apr 28 22:55:10 stm32mp1 systemd-logind[650]: Removed session c2.
May 14 13:26:18 stm32mp1 systemd-logind[650]: New session c3 of user lservice.
May 14 13:39:24 stm32mp1 systemd-logind[650]: Removed session c3.
May 27 11:58:58 stm32mp1 systemd-logind[650]: New session c4 of user root.
列表 12-1:用户会话的典型 journald 条目
日志清晰地显示了 root 用户在 4 月 28 日有两次会话,以及本地服务用户lservice在 5 月 14 日登录了大约 13 分钟。如果这与公司技术人员的官方检查不符,可能意味着存在未授权访问。
journald守护进程可以运行在多种模式下,其中两种模式是易失性和持久性。第一种模式仅在系统重启之前存储日志数据,而第二种模式则使journald将日志数据持久化存储在/var/log/journal中,这对于取证可能非常有帮助。你可以在其配置文件/etc/systemd/journald.conf中设置相应的Storage=persistent选项。
进一步的配置参数,如SystemMaxUse=和RuntimeMaxUse=,以及SystemKeepFree=和RuntimeKeepFree=,分别决定了磁盘和内存存储的日志文件大小限制,以避免因日志文件占用过多内存而引发的关键问题。
使用 auditd 进行内核事件监控
Linux 内核提供了一个审计框架,用于收集内核中可能涉及安全的事件信息。这个框架与 MAC 系统(如 SELinux 或 AppArmor)使用的框架相同,后者用于监视和限制对系统资源的访问,如第十一章中所述。
auditd用户空间工具基于该框架构建,并使设备的非易失性内存中能够持久存储安全事件。
安装
如果你已经成功安装了 AppArmor 或其他流行的 MAC 系统,那么审计框架可能已经启用并在运行中。否则,你可以在 Linux 内核的配置文件中设置CONFIG_AUDIT=y来启用它。
Yocto 的meta-oe层包含了安装auditd工具的配方,来自meta-oe/recipes-security/audit。你可以通过设置IMAGE_INSTALL += "auditd"将其简单地添加到你的镜像中。在使用新安装的软件启动系统后,你应该能看到系统中包含了/var/log/audit/audit.log文件,这是默认的持久化存储内核审计数据的位置。
定制化
审计框架是一个强大的工具,能够深入访问系统进程,因此,明确指定你要监控的事件类型非常重要。你可以使用auditctl向正在运行的auditd服务添加监视规则,如清单 12-2 中所示。
# auditctl -D
No rules
# auditctl -w /etc/passwd -p rwa -k users_credentials
# auditctl -l
-w /etc/passwd -p rwa -k users_credentials
清单 12-2:使用 auditctl 添加审计规则
-D参数会删除所有当前活动的规则。清单 12-2 中的第二个命令安装了一个规则,用于监视/etc/passwd文件(-w /etc/passwd)的读取(r)和写入(w)访问以及文件属性更改(a)。-k选项允许添加一个任意的键字符串,为规则赋予特定意义,并加速后续日志的搜索。
然而,auditctl设置的规则仅在系统重启之前有效。为了持久性地安装监视规则,你必须将它们添加到/etc/audit/audit.rules文件中。如清单 12-3 所示,该文件中的规则实际上等同于命令行中调用auditctl的内容,只是开头没有工具名称。
## SSH daemon configuration
-w /etc/default/dropbear -p wa -k ssh_config
-w /etc/dropbear -p rwa -k ssh_config
## Users and credentials
-w /etc/passwd -p rwa -k users_credentials
-w /etc/shadow -p rwa -k users_credentials
## auditd configuration
-w /etc/audit/ -p wa -k auditd_config
## Audit management tools
-w /sbin/auditctl -p x -k audit_tools
-w /sbin/auditd -p x -k audit_tools
清单 12-3:持久性审计规则示例
文件访问监视在多种场景下都非常有用。例如,如果你基于流行的dropbear守护进程启用了 SSH 访问设备,那么监视相应配置文件的变化是合理的,因为如果该文件被篡改,可能会为你的设备打开一个后门。此外,访问/etc/passwd和/etc/shadow通常是恶意活动的指示。高级攻击者也可能会攻击审计系统本身,这使得包含审计规则和审计工具的文件成为有效的监控目标。
启用规则后,你可以使用ausearch工具来过滤审计数据,正如清单 12-4 中展示的,针对已安装的 SSH 配置监视规则。
# ausearch -k ssh_config
time->Thu Apr 28 23:12:57 ...
type=PROCTITLE msg=audit(1651187577.140:366): proctitle=...
type=SYSCALL msg=audit(1651187577.140:366): arch=40000028 syscall=290 ...
... uid=0 gid=0 ... comm="auditctl" exe="/sbin/auditctl" ...
type=CONFIG_CHANGE ... op=add_rule key="ssh_config" ...
# touch /etc/default/dropbear
# ausearch -k ssh_config
...
time->Thu Apr 28 23:24:47 ...
type=PROCTITLE msg=audit(1651188287.600:379): proctitle=...
type=PATH msg=audit(1651188287.600:379): ... name="/etc/default/dropbear" ...
type=PATH msg=audit(1651188287.600:379): ... name="/etc/default/" ...
type=CWD msg=audit(1651188287.600:379): cwd="/home/root"
type=SYSCALL msg=audit(1651188287.600:379): arch=40000028 syscall=322 ...
... uid=0 gid=0 ... comm="touch" exe="/bin/touch.coreutils" ...
清单 12-4:使用 ausearch 查找可疑活动
第一次调用ausearch并使用ssh_config关键字时,报告指出一个新的规则已通过auditctl工具添加。之后,我只是触碰(甚至没有操作)位于/etc/default/dropbear的配置文件。第二次调用ausearch清楚地表明,root 用户(uid=0 gid=0)使用touch工具访问了文件/etc/default/dropbear,而且只是在新规则加载后 12 分钟内,就在/home/root目录下。这一示例展示了对于关键系统文件拥有监控数据的好处。
除了监视文件访问外,auditd还能够拦截系统调用。然而,由于此类规则直接影响性能,因此只有在认为绝对必要时,才应使用它们。
服务和应用程序事件日志记录
虽然auditd提供了强大的服务来监视内核行为并记录对系统资源的访问,但它没有捕获你设备的重要部分:由服务和应用程序生成的特定事件。
使用 syslog 记录应用程序事件
在我的 STM32MP157F 固件中,BusyBox 实现了syslogd的一个最小版本,某些应用程序使用它将日志信息存储在/var/log/messages文件中。一个这样的应用程序示例是轻量级的dropbear SSH 守护进程。
清单 12-5 展示了多次 SSH 登录尝试后的对应日志信息。
# grep /var/log/messages -e dropbear
Apr 29 09:06:38 stm32mp1 authpriv.info dropbear[819]:
Child connection from ::ffff:192.168.13.17:37214
Apr 29 09:06:40 stm32mp1 authpriv.notice dropbear[819]:
pam_unix(dropbear:auth): ... rhost=::ffff:192.168.13.17 user=rservice
Apr 29 09:06:42 stm32mp1 authpriv.warn dropbear[819]:
pam_authenticate() failed, rc=7, Authentication failure
Apr 29 09:06:42 stm32mp1 authpriv.warn dropbear[819]:
➊ Bad PAM password attempt for 'rservice' from ::ffff:192.168.13.17:37214
Apr 29 09:06:51 stm32mp1 authpriv.notice dropbear[819]:
➋ PAM password auth succeeded for 'rservice' from ::ffff:192.168.13.17:37214
Apr 29 09:07:01 stm32mp1 authpriv.info dropbear[819]:
➌ Exit (rservice) from <::ffff:192.168.13.17:37214>: Disconnect received
Apr 29 09:07:11 stm32mp1 authpriv.info dropbear[825]:
Child connection from ::ffff:192.168.13.17:48656
...
Apr 29 09:07:16 stm32mp1 authpriv.warn dropbear[825]:
Bad PAM password attempt for 'root' from ::ffff:192.168.13.17:48656
...
Apr 29 09:07:20 stm32mp1 authpriv.warn dropbear[825]:
Bad PAM password attempt for 'root' from ::ffff:192.168.13.17:48656
...
Apr 29 09:07:23 stm32mp1 authpriv.warn dropbear[825]:
Bad PAM password attempt for 'root' from ::ffff:192.168.13.17:48656
Apr 29 09:07:23 stm32mp1 authpriv.info dropbear[825]:
➍ Exit before auth from ...: (user 'root', 3 fails): Exited normally
清单 12-5:通过 syslogd 记录的 SSH 登录尝试
日志数据首先显示了一个 Bad PAM password attempt ➊,是针对用户 rservice 的,紧接着是同一用户的成功 PAM password auth ➋。建立的会话仅在 10 秒后 ➌ 被关闭。此外,还可以看到对 root 用户的三次失败登录尝试,最终导致 SSH 守护进程退出登录过程 ➍。这样的日志数据对于检测 SSH 账户的暴力破解攻击或来自不寻常用户和意外 IP 地址的登录尝试非常有用。
使用特定应用程序的日志文件
有些应用程序默认不依赖于 syslogd 的服务,而是创建和管理自己的日志文件。一个例子是运行在我的 STM32MP157F-DK2 板上的 apache2 web 服务器。它的日志文件 access_log 和 error_log 可以在 /var/log/apache2/ 下找到。Listing 12-6 显示了来自 IP 地址 192.168.13.17 的示例 HTTP 请求。
# cat /var/log/apache2/access_log
...
192.168.13.17 - - [29/Apr/...09:35:46 +0000] "GET / HTTP/1.1" 200 45
192.168.13.17 - - [29/Apr/...09:35:58 +0000] "GET /admin/ HTTP/1.1" 404 196
192.168.13.17 - - [29/Apr/...09:36:27 +0000] "GET /config/ HTTP/1.1" 404 196
192.168.13.17 - - [29/Apr/...09:37:29 +0000] "GET /cgi-bin/ HTTP/1.1" 404 196
192.168.13.17 - - [29/Apr/...09:38:33 +0000] "GET / HTTP/1.1" 200 45
...
Listing 12-6: 来自 apache2 的示例访问日志条目
这些信息不仅提供了 web 服务器成功交付的网页的历史记录,还揭示了可能暴露典型侦察阶段的敌对活动的请求。在这里,攻击者检查了 /admin/、/config/ 和 /cgi-bin/ 子目录的可用性。再次强调,早期检测可以防止后续攻击和损害。
日志记录到远程服务器
所有前面提到的工具都在设备上本地运行,当你拥有设备的物理或远程访问权限时,这可能足以满足设备特定的取证需求。然而,正如本章前面所提到的,集中收集日志数据在许多方面是有益的。虽然建立中央监控基础设施超出了本书的设备中心化重点,但我想指出的是,所有这些工具收集的数据都可以传输到远程位置。
例如,journald 守护进程可以通过一个名为 systemd-journal-remote 的服务扩展,用于将数据记录到远程服务器。audisp 工具及其 audisp-remote 插件可以实现通过 Linux 审计框架和 auditd 收集的数据的中央日志聚合。或者,可以使用像 auditbeat 这样的特定工具将审计数据发送到 Elastic Stack。进一步地,rsyslog 和 syslog-ng 是现代的 syslog 实现,它们允许将日志记录到远程位置。最后,其中一些工具甚至是互操作的,这意味着例如 journald 可以配置为将日志数据转发给一个 syslog 守护进程,后者可以负责远程传输。
总结
请求一个设备聚合并传输系统数据和事件信息到中央服务器是很简单的。然而,当你开始实施这些措施时,你会遇到多个障碍需要克服,从法律问题到设备限制,再到持续运营安全监控服务所需的人力资源。
本章讨论了对嵌入式系统进行安全监控时可能有用的典型信息,从用户会话及其交互到网络流量元数据,再到系统崩溃和错误消息。显然,你想监控的内容越多,就需要更多的设备端和服务器资源。基于风险的方法完全合理,可以在全面的设备透明度和最小资源使用之间找到折衷,适用于你的具体产品。
系统监控是防御深度策略中的一个重要层级,因为即使你实施的所有保护措施都失败,监控仍然可能帮助你检测到正在进行的攻击和异常。监控可以决定是只是受损而有些代价,还是完全毁掉你的产品和公司的声誉。
第十三章:后记

当我决定写这本书时,我的目标是为学生和从业者提供一本有用的、实用的嵌入式系统安全概述,帮助他们在各种威胁、保护措施、安全营销和完全绝望的丛林中找到出路。好吧,我们走到了这里。如果你属于这两个群体之一并已经读到这里,我相信你已经发现了新的领域,可能学到了有用的东西,甚至可能找到了下一个个人挑战。
如果你已经将本书中的主题转化为日常工作,并在你桌面上的特定嵌入式系统中付诸实践,你可以为自己感到自豪。这可能并非易事,且需要不懈的努力。然而,现在还不是庆祝的时候。这仅仅是你安全之旅的开始,并不是故事的结局。付费电视、汽车和游戏主机等行业已经意识到,只要有足够大的利益可图,攻击者会不惜一切代价找到它。
在过去的二十年里,侧信道分析这一话题逐渐从学术领域走向了某些特定行业。这些攻击利用了加密算法和其他保护措施的物理特性,如运行时间、电力消耗或电磁辐射,来提取关于秘密和微芯片内处理的中间值的信息。尽管这种方法需要专门的知识以及适当的硬件和软件,但它已经在付费电视系统、电子锁和汽车组件等领域得到了实际应用,仅举几个例子。
然而,如果你必须面对如此强大的攻击者,你并不迷失。你有一系列反制措施和保护实施方案可以选择。
斯特凡·曼加德、伊丽莎白·奥斯瓦尔德和托马斯·波普撰写了《功耗分析攻击:揭示智能卡的秘密》(Springer,2007),这是该领域的第一本书之一,如果你对该主题的基础知识感兴趣,可以参考。此外,贾斯珀·范·沃登贝格和科林·奥弗林(本书技术审阅者)所著的《硬件黑客手册》(No Starch Press,2021)包含了多种好的和不好的实现实例,并提供了提升个人侧信道分析技能的技巧。
另一类强大的威胁是故障注入攻击,这些攻击通过电压或时钟故障、电磁脉冲,甚至激光束,故意迫使微芯片跳过指令或处理损坏的数据。听起来像科幻小说吗?这些攻击显然需要具备高水平知识和专业设备的对手,但它们已经在一些游戏主机上在实际环境中实施过——例如,在安全启动过程中绕过固件认证验证。之后,攻击者能够在这些设备上启动并执行他们自定义的软件。同样,关于这个话题的实际介绍,包括现实世界的例子,可以在《硬件黑客手册》中找到。
除了对硬件层面的高级攻击外,软件层面的复杂性上升也导致了攻击面增加。因此,针对具有不同重要性的应用程序进行安全软件分段,是复杂软件架构中设备的另一个后续安全话题。例如,一些产品已经实现了虚拟化或容器技术,将用户定义的软件与厂商的系统应用程序隔离开来。突然间,像应用程序突破其受限环境这样的威胁——我们在云系统中也见过——变得对嵌入式系统来说同样相关。此外,旨在提供较高安全性的设备可能将关键软件应用迁移到专用的安全执行环境中,如 ARM TrustZone。但这也需要硬件支持、特定的固件架构以及大量的实施经验,这些都增加了系统复杂性,并为进一步的攻击向量提供了便利。简而言之,你永远不会感到无聊。
尽管如此,始终记住一点:工程设计安全设备并不需要魔法般的力量。你所需要的只是那些充满热情、富有动力且聪明的人,他们能够作为一个团队共同合作。加油!


浙公网安备 33010602011771号