代码改变世界

3.2 为程序集分配强名称

2011-12-02 11:27  iRead  阅读(642)  评论(0编辑  收藏  举报

  要由多个应用程序访问的程序集必须放到一个已知的目录中。另外,检测到对该程序集的一个引用时,CLR必须能自动检查这个目录。但现在的问题是:两个(或者更多)公司可能生成具有相同文件名的程序集。所以,假如两个程序集都复制到同一个移植的目录,最后一个安装的就是“老大”,造成正在使用旧程序集的所有应用程序都无法正常工作(这正是Windows的“DLL Hell”现象的根源,因为共享DLL全部被复制到System32目录中)。

   显然,只根据文件名来区分程序集是不够的。CLR必须提供对程序集进行唯一性标识的机制。这正是“强命名程序集”的来历。一个强命名的程序集具有4个重要attribute,它们共同对程序集进行了唯一性标识:一个文件名(不计扩展名)、一个版本号、一个语言文化(culture)标识以及一个公钥。由于公钥是非常大的数字,所以经常使用从公钥派生的一个小的哈希值。这个哈希值称为公钥标记(public key token。以下程序集标识字符串(有时称为程序集显示名称)标识了4个完全不同的程序集文件:

  “MyTypes,Version=1.0.8123.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”

  “MyTypes,Version=1.0.8123.0,Culture=”en-US”,PublicKeyToken=b77a5c561934e089”

  “MyTypes,Version=2.0.1234.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”

    “MyTypes,Version=1.0.8123.0,Culture=neutral,PublicKeyToken=b03f5ff11d50a3a”

   第一个字符串标识了一个名为MyTypes.exe或者MyTypes.dll的程序集文件(无法根据“程序集标识字符串“判断文件的扩展名)。生成该程序集的公司为其分配的版本号是1.0.8123.0,而且程序集中没有任何内容与一种特定的语言文化关联,因为Culture设为neutral。当然,任何公司都可以生成一个MyTypes.dll(或者MyTypes.exe)程序集文件,为其分配相同的版本号1.0.8123.0,并将语言文化设为中性。

  所以,必须有一种方式区分恰好具有相同attributes的两个公司的程序集。考虑到几个方面的原因,Microsoft选择使用标准的公钥/私钥加密技术,而不是使用其他唯一性标识技术,比如GUID(全局唯一标识符)、URL(统一资源定位符)或者URN(统一资源名)。具体地说,使用加密技术,不仅能在程序集安装到一台机器上时检查其二进制数据的完整性,还允许每个发布者都能授予一套不同的权限。本章稍后会讨论这些技术。

   所以,一个公司要想唯一性地标识它的程序集,必须创建一个公钥/私钥对。然后,公钥可以同程序集关联。没有任何两家公司有相同的公钥/私钥对。这样一来,两家公司就可以创建具有相同名称、版本和语言文化的程序集,同时不会造成任何冲突。

注意:System.Reflection.AssemblyName类是一个辅助类,可利用它轻松构建一个程序集名称,并获取程序集名称的各个组成部分。类提供了几个公共实例属性,比如CultureInfo,FullName,KeyPair,Name和Version。这个类还提供了几个公共实例方法,比如GetPublicKey,GetPublicKeyToken,SetPublicKey和SetPublicKeyToken。

  第2章展示了如何命名程序集文件,以及如何应用程序集版本号和语言文化。弱命名的程序集可在清单元数据中嵌入程序集版本和语言文化等attributes;然而,当CLR探测子目录来查找附属程序集(statellite assembly)时,会忽略版本号,只使用语言文化信息。

  由于弱命名程序集总是私有部署的,所以当CLR在应用程序的基目录或者子目录1中搜索程序集的文件时,只会使用程序集的名称(添加一个.dll或者.exe扩展名)。

  强命名程序集则有一个文件名,一个程序集版本号和一个语言文化(culture)。除此之外,强命名程序集还是用发布者的私钥进行了签名。

  创建强命名程序集的第一步是使用Strong Name实用程序(SN.exe)来获取一个密钥,这个实用程序是与.NET Framework SDK和Microsoft Visual Studio配套提供的。SN.exe允许指定多种命名行开关来使用一整套功能。注意,SN.exe的所有命令行开关都区分大小写。要生成一个公钥/私钥对,要像下面这样运行SN.exe:

   SN –k MyCompany.snk

这一行告诉SN.exe创建一个名为MyComany.snk的文件。文件中包含二进制形式的公钥和私钥。

  公钥数字非常大;如果愿意,在创建了包含公钥和私钥的文件之后,可再次使用SN.exe实用程序来查看实际的公钥。为此,必须执行两次SN.exe实用程序。首先,要用-p开关执行SN.exe,创建一个只包含公钥的文件(MyCompany.PublicKey):

  SN –p MyCompany.snk MyCompany.PublicKey

  接着用-tp开关执行SN.exe,并指定只包含公钥的文件:

  SN –tp MyCompany.PublicKey

  上述命令行的输出如下:

         Microsoft®.Net Framework 强名称实用工具 版本4.0.30319.1

         Copyright© Microsoft Corporation. All rights reserved.

         公钥为

    0024000004800000940000000602000000240000525341310004000001000100bdde0a979dfce0

    8befa02c9eef20b759e8990e28de1ecc22a6c301a1f45fd7123e90e3b5c359627c3504524b48d3

    16334e3f9f794c05da84f984ad017e367a2a6206cb04bd4cdb28b99c18a26918b85cb954e7e8f7

    ae1f37a12433faf948c1c8babe44de0194d8a64fe94ce834a8716436be23f32851554e42339fd6

    06b5b7bd

  公钥标记为cab91bded3652791

  注意,SN.exe实用程序未提供任何方式显示私钥。

  由于公钥太大,难以使用,为了简化开发人员的工作(也为了方便最终用户),人们设计了公钥标记(public key token)。公钥标记是公钥的64位哈希值。SN.exe的-tp开关在输出结果的末尾显示了与完整公钥对应的公钥标记。

   知道了如何创建一个公钥/私钥对之后,可以非常简单地创建一个强命名程序集。编译程序集时,要是用/keyfile:<file>编译器开关:

  csc /keyfile:MyCompany.snk app.cs

  C#编译器看见这个开关,就会打开指定的文件(MyCompany.snk),用私钥对程序集进行签名,并将公钥嵌入清单。注意,只能对包含清单的程序集文件进行签名;程序集的其他文件不能被显示地签名。

  要在Visual Studio中新建一个公钥/私钥文件,可显示项目属性,进入“签名”选项卡,勾选“为程序集签名”,然后从“选择强名称密钥文件”框中选择”<新建…>”。

  当我们说“对一个文件进行签名”时,确切的含义是:生成一个强命名程序集时,程序集的FileDef清单元数据列出了构成程序集的所有文件,每次将一个文件的名称添加到清单中,文件的内容都会进行哈希处理,得到的哈希值会和文件名一道存储在FileDef表中。要想覆盖默认哈希算法,可使用AL.exe的/algid开关,也可在程序集的某个源代码文件中,在assembly这一级上应用System.Reflection.AssemblyAlgorithmAttribute这个定制attribute。默认使用的是SHA-1算法,它对几乎所有应用程序来说都足够了。

  生成了包含清单的PE文件后,会对PE文件的完整内容(任何Authenticode Signature、程序集强名称数据以及PE头校验和除外)进行哈希处理,如图3-1所示。此时使用的哈希算法始终是SHA-1,而且不可更改。这个哈希值使用发布者的私钥进行签名,最终得到的RSA数字签名会存储到PE文件的一个保留区域中(进行哈希处理时,会忽略这个区域)。PE文件的CLR头会进行更新,反映出数字签名在文件中的嵌入位置。

图3-1 对程序集进行签名

  发布者公钥也嵌入这个PE文件的AssemblyDef清单元数据表中。文件、程序集版本号、语言文化以及公钥的组合为这个程序集赋予了一个强名称,它保证是唯一的。两家公司除非共享密钥对,否则即使都生成了一个名为Calculus的程序集,公钥/私钥也不可能相同。

  到此为止,程序集及其所有文件就可以开始打包和分发了。

   如第2章所述,编译源代码时,编译器会检测到代码引用的类型和成员。必须向编译器指定要引用的程序集。对于C#编译器,要使用/reference编译器开关。编译器的一部分工作是在最终的托管模块中生成一个AssemblyRef元数据表。AssemblyRef元数据表的每个记录项都指出了被引用的程序集的名称(无路径和扩展名)、版本号、语言文化和公钥信息。

重要提示:

  由于公钥是非常大的数字,而一个程序集可能引用其他大量程序集,所以在最终生成的文件中,相当一部分会被公钥信息占据。为了节省存储空间,Microsoft对动摇进行哈希处理,并获取哈希值的最后8个字节。AssemblyRef表示及存储的是这种简化的公钥值(成为“公钥标记”)。开发人员和最终用户一般看到的都是公钥标记,而不是完整公钥。

  但要注意,CLR在做出安全或信任决策时,永远都不会使用公钥标记,因为几个公钥可能在哈希处理之后得到同一个公钥标记。

  第2章讨论过的JeffTypes.dll文件的AssemblyRef元数据信息(使用ILDasm.exe获得)如下:

  

AssemblyRef #1 (23000001)

-------------------------------------------------------

Token: 0x23000001

Public Key or Token: b7 7a 5c 56 19 34 e0 89

Name: mscorlib

Version: 4.0.0.0

Major Version: 0x00000004

Minor Version: 0x00000000

Build Number: 0x00000000

Revision Number: 0x00000000

Locale: <null>

HashValue Blob:

  可以看出,JeffTypes.dll引用了一个类型,该类型包含在一个程序集中,该程序集匹配以下attributes:

  “MSCorLib,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”

  遗憾的是,ILDasm.exe在本应使用Culture的地方使用了Locale。检查JeffTypes.dll的AssemblyDef元数据表,会看到以下内容:

Assembly

-------------------------------------------------------

Token: 0x20000001

Name : JeffTypes

Public Key    :

Hash Algorithm : 0x00008004

Version: 3.0.0.0

Major Version: 0x00000003

Minor Version: 0x00000000

Build Number: 0x00000000

Revision Number: 0x00000000

Locale: <null>

Flags : [none] (00000000)

  它等价于:

  “JeffTypes,Version=3.0.0.0,Culture=neutral,PublicKeyToken=null”

  之所以没有指定公钥标记,是因为第2章创建JeffTypes.dll程序集时,没有使用一个公钥/私钥对进行签名,这使它成为一个弱命名程序集。如果使用SN.exe创建一个密钥文件,再用/keyfile编译器开关来编译,最终的程序集就是经过签名的。使用ILDasm.exe来查看新程序集的元数据,AssemblyDef记录项就会在Public Key字段之后显示相应的字节,表明它是一个强命名程序集。顺便说一句,AssemblyDef记录项总是存储完整的公钥,而不是公钥标记。之所以需要完整公钥,是为了保证文件没有被篡改。本章后面将解释强命名程序集如何防篡改。