“TestLib,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = 769a8f10a7f072b4”
如果你能看懂上面这行的意思,那你很可能是一个.NET开发人员,同时你也可能知道结束处的十六进制字符串表示的是一个公钥标记。
不错,上面的字符串就是一组.net程序集强名称签名。
但你知道如何计算这个令牌吗?你知道强名称签名的结构吗?在这篇文章中,我们将详细介绍强名称的工作原理及其优缺点。
强名称是由程序集的标记加上公钥和数字签名组成的。其中,程序集的标记包括简单文本名称、版本号和区域性信息(如果提供的话)。也就是说,一个完整的强名称字符串包含4部分:中国菜刀
1.程序集的文件名;
2.程序集的版本号;
3.程序集的区域性信息;
4.一个公钥以及一个数字签名;
其中前3部分信息会存储在程序集的清单(manifest)中。清单包含了程序集的元数据,并嵌入在程序集的某个文件中。它使用相应私钥从程序集文件生成。
具有强名称的程序集只能使用其他具有强名称的程序集的类型。 否则,将会危害具有强名称的程序集的安全性。
在.NET框架中强名称程序集是通过公钥和私钥加密来产生这个唯一标记的。
因为强命名程序集使用公钥/私钥对来进行唯一性签名,不同的公司公钥/私钥对不可能相同,所以所生成的程序也不相同,这就解决了以前老出现的DLL Hell问题(两个不同的公司可能开发处具有相同名称的程序集,如果将相同名称的程序集放置到同一个目录下,则会出现程序集覆盖现象,最后安装的程序集会覆盖前面的程序集,从而可能导致应用序不能正常运行)。任何两个强命名的程序集,就算名称一模一样,Windows也知道他们是两个不同的版本,因为他们的唯一标记不一样。并且你可以通过配置文件控制应用程序去正确的加载你想要加载的那个dll。
所以强名称签名还能唯一标记程序集所使用的组件,所以强名称的作用主要有三个:
1.区分不同的程序集;
2.确保代码没有被篡改过;
3.在.NET中,只有强名称签名的程序集才能放到全局程序集缓存中。
公钥标记和强名称签名
公钥是包含在程序集元数据中的#Blob流的一部分,下图就是dnSpy窗口的一部分,dnSpy是一款开源的基于ILSpy发展而来的.net程序集的编辑,反编译,调试神器:
下图列出了构建公钥模块的元素:
上图中记录有公钥标记,开发人员和最终用户看到的也是公钥标记,而不是公钥。
公钥和公钥标记有什么区别呢?
公钥总共占160字节,有32个字节装的是头信息,另外有128个字节装的是数据。公钥对程序开发者来说是可见的。因为公钥占160字节,一个程序集可能会引用很多其他的程序集,所以在最终生成的文件中会有很大一部分空间被公钥占用,在使用的时候不太方便,于是人们提出了一个公钥标记的概念,公钥标记只占8个字节。公钥标记是把公钥进行哈希处理(使用SHA1算法),把哈希处理的结果的后面8个字节进行逆转,得到的就是公钥标记,天空彩
本文的测试中所用的公钥标记为:
769a8f10a7f072b4 (SHA-1(00 24 00 00 0c 80 … c8 8a c1 b1) = 9aa4de0a96ada8d83d6d7678b472f0a7108f9a76
怎样对程序集进行RSA签名
1.对PE文件内容进行哈希处理,然后把哈希处理后的值,用私钥进行RSA签名,把签名后的值加入到PE文件的CLR头中去。同时也会把公钥加入到程序集的元数据中去。
2.在生成元数据表FileRef时,CLR会把程序集里面的文件也进行哈希处理,得到一个哈希值,把文件和对应的哈希值同时加到FileRef元数据表中,
为了计算RSA签名,我们需要拥有与公钥相对应的私钥。如果你想看到实现验证的C#代码,请查看dnLib库中的StrongNameSigner.cs文件。
在研究了强名称签名结构之后,让我们来了解一下可以用来创建强名称签名的工具。我们发现生成的.snk文件会存储RSA密钥详细信息(如果你对.snk文件格式的详细信息感兴趣,请查看010编辑器模板):
然后我们可以使用C#编译器(csc.exe)或组装链接器(al.exe),这两个两者都可以被注入/ keyfile参数,在这里我们提供了刚生成的.snk文件的路径,如下图:
此命令将基于文件内容的SHA-1算法生成签名。不过现在,SHA-1已经不是一种安全的算法了,强烈建议使用SHA-2算法。
如果要使用SHA-2算法对我们的程序集进行签名,我们首先需要提取.snk文件的公钥部分:
然后延迟私钥签名(强名称签名块将被清0,强名称签名标志不会被置位)。 csc.exe和al.exe都会被注入/ delaysign +参数用于延迟私钥签名:
csc.exe /keyfile:TestLibPubKey.snk /delaysign+ /t:library TestLib.cs
什么是延迟签名?
如果要生成一个强命名的程序集,那么每次生成都需要进行签名,在开发的时候就会频繁的访问私钥文件,而私钥文件一般都是非常保密的,想要频繁使用可能有些费事。所以就有了延迟签名的机制。延迟签名的意思就是在开发阶段,只把公钥提供给开发人员,只有公钥对程序集进行签名,在最后打包发布的时候,才使用私钥来进行签名。
最后,我们需要使用私钥重新对程序集进行签名:
Authenticode 签名
Authenticode 签名,顾名思义,用于验证程序集的开发者身份另外它还能保护组件的完整性。Authenticode 签名的大小和位置存储在PE可选标题中:
强名称虽然引入了“身份”的概念,但没有包括“信任”机制。例如,使用强名称签署的一个程序集虽然能保证版本兼容性,但不能保证要加载的程序集来自Quilogy。为了用Authenticode数字签名来签署程序集,开发者要使用.NET框架配套提供的命令行实用程序Signcode.exe。程序集使用Authenticode签名进行签署之后,管理员就可创建相应的策略,利用代码访问安全性(CAS)机制,允许它下载到用户的机器上并进行加载。签名将成为CLR的类加载器所使用的身份凭证的一部分,用于判断程序集是否应该加载。
程序集可携带完整的 Microsoft Authenticode 签名。 Authenticode 签名包括建立信任的证书。
请务必注意强名称不要求代码以这种方式进行签名。事实上,用于生成强名称签名的密钥不需要与用于生成Authenticode签名的密钥相同。
跳过受信任程序集的签名验证
从.NET Framework 3.5 Service Pack 1开始,当程序集加载到完全信任的应用程序域(如 MyComputer 区域的默认应用程序域)时,不会验证强名称签名。 这被称之为强名称跳过功能。 在完全信任的环境中,对于已签名的完全信任的程序集,对StrongNameIdentityPermission的需求总是成功,而不考虑其签名。 这种情况下,强名称跳过功能可避免完全信任程序集不必要的强名称签名验证开销,允许更快地加载程序集。
跳过功能适用于使用强名称进行签名及具有以下特征的任何程序集:
1.完全受信任,无需 StrongName证据(如具有 MyComputer 区域证据);
2.加载到完全受信任的 AppDomain;
3.加载自该 AppDomain 的 ApplicationBase 属性下的某个位置;
4.签名没有延迟;
总结
由于很容易跳过签名验证,你可能怀疑二进制文件的整个结构。不过,考虑一下,如果恶意攻击者获得对二进制文件的访问权,签名将不能保护你的软件安全。不过,签名程序集是唯一的能证明程序集中的代码是合法的证明,并且没有被篡改。
我们应该在客户端接收到二进制文件(这通常由安装程序完成)后执行第一次验证,以确保在传输过程中没有篡改;接下来,在二进制文件所在的文件夹中设置有效的访问权限非常重要,只有被授权的人才能修改文件;最后,每当有我们的应用程序出现问题,我们应该要求客户端在填写错误报告之前验证签名,只有这样我们才能确定该错误是我们本来应该有的真实错误。