背景故事 随着企业和个人对网络安全的愈发重视,软件的安全开发生命周期(Secure Development Lifecycle)也被越来越多的提及。本篇对SDL常见的信息保密需求,结合.NET中敏感信息保护和存储的实践做简单讨论,其中安全需求部分参考IEC(国际电委员协会)62443-4-2 CR 4.1 Information confidentiality(信息保密)。 IEC62443是国际上被广泛认可的工控系统标准,其中62443-4-2部分描述了在具体技术实现上的信息安全要求。
CR 4.1 Information confidentiality定义的安全需求内容大体翻译如下:
提供保护静态信息机密性的能力,并支持明确的读取授权。 支持保护传输中信息的机密性,如EC 62443-3-3 SR 4.1 标准。 本篇讨论的范围局限于第一点,即对静态信息的保护。在软件程序中,第一点需求通常可以被分解为以下:
敏感信息不能以明文,人类可阅读的格式存储。 加密算法要符合行业规范,不能使用过时的,有缺陷的加密算法。 对敏感信息的加密,应是安装实例(或机器)相关的,而不是相同密钥。 敏感信息的识别 结合.NET应用软件开发的常见场景,假定识别出以下敏感信息,作为示例,分别对具体的保护方案展开讨论。
软件自身的License文件。 身份验证用的JWT security key。 数据库连接密码。 第三方系统的凭证信息(用户名,密码)。 .NET对敏感信息的保护和存储 .NET作为全功能的开发平台,对安全相关内容做了非常好的支持。本篇讨论的敏感信息保护和存储分为两个部分。其中数据保护依赖ASP.NET Core Data Protection API,可理解为对数据的加密和解密。保护后的数据还需要做安全存储,同样依赖.NET Configuration的相关特性。
ASP.NET Core Data Protection API ASP.NET Core Data Protection API(本文简称DPAPI)是一套加密API,可用于数据保护,包括密钥管理和轮换。这里注意一般情况下密钥管理和轮换无需开发人员操作,.NET会处理算法选择和密钥生存周期。
数据保护过程主要包括以下步骤:
从数据保护提供程序(Data Protection Provider)创建数据保护程序(Data Protector)。 对要保护的数据调用Protect方法(加密)。 对要恢复为纯文本的数据调用Unprotect方法(解密)。 随着我们对DPAPI的实际使用,还会发现以下特点,这里先不做展开。
契合.NET 技术栈,使用简单方便。 可实现机器相关,甚至用户相关的非固定密钥加密。 通过ApplicationName和Purpose灵活实现密钥的共享或隔离。 敏感信息的存储 敏感信息在通过DPAPI保护后,可采用以下存储方案之一或组合:
指定目录下的特定文件,例如软件自身的License。 操作系统环境变量,例如JWT security key。 应用程序运行目录下的配置文件,例如开发环境调试用的Password。 数据库表存储,例如第三方系统的用户名,密码。 .NET Configuration 本篇对敏感信息的存储,一定程度上依赖.NET处理配置信息的顺序。在ASP.NET Core中,通过Host对象启动的应用程序按以下顺序读取配置(优先级高到低):
使用命令行配置提供程序通过命令行参数提供。 使用环境变量配置提供程序通过环境变量提供。 应用在 Development 环境中运行时的应用机密。 使用 JSON 配置提供程序通过 appsettings.json 提供。 使用 JSON 配置提供程序通过 appsettings.Environment.json 提供 。 例如,appsettings.Production.json 和 appsettings.Development.json。 通过DPAPI对数据进行保护,结合.NET 读取配置的优先级,针对不同数据区别存储位置,共同构建了本篇对于敏感信息保护和存储的实践。
下面根据适用场景将做区分讨论。示例如无特殊说明,均为Windows单服务器部署。Application / Web Service可以是安装包程序,桌面程序,Web应用程序和Windows Service。
单一应用程序的数据保护和存储 单个Application / Web Service中的数据保护。 多个Application / Web Service,但Protect/Unprotect操作仅在单个Application/Web Service发生。 Application / Web Service不想承担管理密钥的职责。 单一应用程序数据保护的定义应符合以上描述,示例场景如下:
某工控软件在年度渗透测试中,得到反馈:软件自身License的内容不能以明文存储。
对软件自身License的加密,识别为单一应用程序数据保护。
要求对License的内容做用户或机器相关的加密。 要求License文件在工控软件安装后,由用户操作生成。 要求License文件存储位置为指定路径。 数据保护 使用默认设置的ASP.NET Core DPAPI对License文本进行Protect和Unprotect操作。具有如下特点:
无需考虑任何设置,使用成本极低。 密钥是用户相关的,安全性有保障。 这里需要说明的是,在默认设置下,DPAPI对数据的保护是用户相关的。
以Windows举例,可能是运行软件,执行License操作的当前登录账户。也可能是安装过程中,申请的具有安装权限的特定系统用户(如LocalSystem)。这取决于软件具体的操作逻辑。
但要注意,默认情况下,不同用户是无法成功Unprotect,互相解密读取数据的。
数据存储 单一应用程序的数据存储一般无要求,上述示例中,License文件可以指定存放固定位置,或由用户指定位置。
多应用程序单用户共享保护数据的场景 多个Application / Web Service 对同一份数据做 Protect / Unprotect 操作。 多个Application / Web Service 的用户相同。 被保护数据能够被多个Application / Web Service 共享访问。 多个Application / Web Service 均不承担密钥的管理职责。 符合以上描述的示例场景如下:
某内网部署的工业软件,由多个Application / Web Service组成,均存在对数据库Password的访问。
数据库Password对不同客户,应做机器相关加密,而不是所有安装实例使用相同密钥。 多个Application / Web Service访问相同被保护的数据库Password。 多个Application / Web Service部署于同一服务器,使用相同用户(假定为LocalSystem)运行。 要求Password存储到指定位置。 对DPAPI用来Protect的加密密钥位置无约束(密钥位置需要能够被Application / Web Service访问)。 数据保护 和单一应用程序不同,多个Application / Web Service需要以相同账户(假定为LocalSystem)运行,因为密钥和用户相关联。
同时需要注意,DPAPI要以相同ApplicationName创建和共享同一个Data Protection Provider。
并且使用这个相同的Data Protection Provider,以相同的Purpose,创建和共享同一个Data Protector。
数据存储 数据库Password属于敏感信息,要求做用户或机器相关加密,不能以相同密钥加密后,预先写入appsettings.json。
相比在安装过程中用户填写密码,加密后再覆盖配置文件appsettings.json,使用环境变量则更为简单便捷。
根据.NET 配置的优先级顺序。环境变量中对应配置项会自动覆盖配置文件。
区分Development和Production环境 使用DPAPI+环境变量能够满足对敏感信息的数据保护和安全存储。
但实际工作中,开发团队的小伙伴反馈以下问题:
本地开发也需要Protect Password存储到环境变量。 远程连接开发环境服务器调试,Password无法Unprotect。因为Protect是用户相关的。 以上问题影响了开发效率。我们需要区分Dev和Prod环境,改进对敏感数据保护和存储的方案。
本地开发、DIT Server,使用固定密钥。支持本地代码 + DIT环境Server调试。 SIT Server、安装包版本,使用用户相关的加密。 数据保护 根据Environment.IsDevelopment()区分数据保护的方式。 FixedKeyInfoProtector将使用固定密钥的加密方式,相同密钥将产生相同的密文。 DynamicKeyInfoProtector将使用DPAPI做数据保护,用户相关的密钥产生密文不固定。 数据存储 Development环境使用appsettings.development.json的配置项。 Development环境同样支持使用环境变量覆盖配置文件配置项。 非Development环境要求必须使用环境变量中的配置项。 多用户场景对相同数据的保护和存储 多Application / Web Service的用户可能是特定Windows用户,LocalSystem和LocalService的任意组合。 多Applicatoin / Web Service需要共享密钥。 以同一高权限账户来运行所有程序,无论从开发者还是使用者角度,都是一件轻松的事情。但同样根据IEC 62443-4-2 CR 2.1 Authorization enforcement的需求,我们应该只分配最小权限给到人或者进程。所以我们会存在不同进程不同用户的场景。
以常见的Windows Service部署方式举例,默认情况下可能都是以高权限的LocalSystem账户运行。但理论上绝大多数Windows Service都可以被降级为Local Service账户。
数据保护 相同ApplicationName,以共享同一个DataProtectorProvider。 相同Purpose,以共享同一个DataProtector。 DPAPI的密钥存储位置,要求多Application / Web Service能够访问,以共享同一个密钥。 对密钥本身可再做一次LocalMachine相关的加密(注意这里是机器相关,而不是用户相关)。 数据存储 同样要求通过环境变量存储敏感数据,如Password。 环境变量由用户(假定为LocalSystem)环境变量变更为System环境变量。 使用非固定密钥的Development环境无以上要求。 非共享密钥向共享密钥场景的升级 原则上不推荐上述场景的升级,就和接口设计一样,敏感数据保护的安全设计,从最初就应该是稳定的,而不是轻易改变的。如果密钥的变更在我们的设计中是可以轻易实现的,那么别有用心的用户突破约束也不会是一件很难的事情。
升级理论上存在以下场景:
单Application简单保护,向多Application / Web Service单用户的升级。 单Application简单保护,向多Application / Web Service多用户的升级。 多Application / Web Service单用户保护,向多Application / Web Service多用户的升级。 在不变更用户的情况下,处理起来较为简单。核心原则就是为多Application/ Web Service共享ApplicationName和Purpose。ApplicationName和Purpose并不是敏感信息,升级也需要通过修改源代码来重新发布程序。
变更用户则要复杂许多,我们需要通过原用户使用原ApplicationName和Purpose对数据做Unprotect。
再通过新用户产生新密钥到多Application / Web Service共享的位置,然后还需要新用户使用新ApplicationName和Purpose对数据做Protect。
在生产环境,这通常会导致一系列灾难的发生。我希望你永远不需要这么做。
一些值得关注的点 ASP.NET Core依赖注入拿到的ProtectorProvider,会基于应用程序的Content Root路径做应用隔离。使得不同程序的Protector对象不共享。 即使共享物理密钥,ApplicationName不同仍会被隔离。 DPAPI密钥存储的位置和Windows以及用户相关,不完全可控。密钥将保存到 %LOCALAPPDATA%\ASP.NET\DataProtection-Keys 文件夹 LocalSystem用户对应的位置C:\Windows\System32\config\systemprofile\AppData\Local\ASP.NET\DataProtection-Keys 给环境变量添加前缀能够有效避免重名 LocalSystem的用户环境变量,双刃剑。普通用户不可见,可有效避免来自用户的修改。 对开发调试不友好。