.Net学习爱好者的温馨家园

.Net互动平台

导航

.NET安全编程

.NET Framework 是为满足商业组织和个人需求、为支持多种应用程序模型而开发的一种灵活的通用计算平台。.NET汇集了业界发展的最新趋势,它支持高度分布式系统、基于组件的应用程序和基于Web 的服务器解决方案(包括XML Web 服务)。这些新趋势改进了应用程序的功能,提高了程序员的效率,但是它们也要求软件用户、软件制造商和服务提供商密切关注软件和系统的安全。

从传统角度来看,程序员都把安全看做是“马后炮”;然而,现在越来越多人认为安全是一种需求而不是一种选择,因为现在的应用程序都需要把安全集成到它们的开发过程中。这是一个简单的事实,即在开发.NET应用程序时不能忽视安全,因为安全位于.NET Framework 的核心并隐含于编写的应用程序中。即使仅为了响应.NET Framework 的默认操作,也必须理解如何对.NET 安全性进行编程;更重要的是,这样做是为了编写深受欢迎的有效且实用的.NET 应用程序。

本书组织结构

本书分为5 部分。第I 部分介绍了软件安全的基本概念。第II 部分讨论了如何编写.NET 运行库的安全功能。第III 部分阐述了如何以编程方式使用.NET类库中的密码类。第IV 部分讨论了如何使用与程序运行平台有关的安全功能。最后,第V 部分介绍了本书中的安全类涉及的所有API 参考。

第I 部分:基础

第1 章安全基础

本章介绍了软件安全的一些基本概念,在阅读后续章节之前应该理解这些概念。本章说明了安全的必要性和软件安全的目的。本章还介绍了在开发自己的安全编程技术时应注意的一些重要环节。

第2 章程序集

本章概述了.NET 程序集,它是.NET 安全的一个关键组件。本章阐述了程序集的结构和内容,示范如何创建不同类型的程序集,以及讨论如何保护程序集不被篡改和逆向工程。

第3 章应用程序域

本章介绍了应用程序域的作用,讨论了应用程序域对应用程序隔离、安全和配置的影响。

第4 章安全应用程序的生命周期

本章介绍了软件安全性以何种方式集成进应用程序的生命周期中,并且提供了实际的建议以帮助读者理解后续章节所涉及的内容。

第II部分:.NET安全性

第5 章运行库安全性介绍

本章介绍了.NET 运行库提供的主要安全功能;解释了它们的目的和作用,它们如何交互,以及它们与底层操作系统提供的安全之间有何关系。

第6 章证据和代码标识

本章介绍了什么是证据,证据从何而来,用证据的目的及如何使用不同类型证据(包括.NET Framework提供的标准证据类集合)。本章还示范了如何在编程中使用证据,如何在开发用户证据类时扩展.NET Framework 的安全功能。

第7 章权限

本章介绍了什么是权限及其在实现代码访问安全性时的作用。本章还描述了运行库使用何种机制实现代码访问安全性,如何使用权限操作这些机制。最后,本章为读者介绍了在实现自己的用户权限时如何扩展代码访问安全性。

第8 章安全策略

本章介绍了.NET运行库如何使用安全策略来决定授予一个程序集或应用程序域哪类权限,安全策略的结构及运行库的组件如何交互。本章还说明了如何在编程时使用安全策略,并示范如何使用应用程序域策略。

第9 章管理代码访问安全性

本章概述了.NET Framework 运行的默认安全策略,讨论了如何使用.NET 的安全工具来管理安全策略。

第10 章基于角色的安全性

本章讨论了什么是基于角色的安全和执行.NET Framework。本章还阐述了用于访问基于角色安全的类,示范如何在程序中使用这些类。

第11 章隔离存储器

本章介绍了什么是隔离存储器,说明了隔离存储器会给数据存储选项带来何种优势。本章还示范了如何在编程中使用隔离存储器,以及如何管理和控制对隔离存储器的访问。

第III部分:.NET加密

第12 章介绍加密

本章概述了加密的不同方面,讨论了加密应该注意的一些危险和限制。

第13 章散列算法

本章深入探讨了散列码,如何使用.NET Framework 类库创建和运行散列码,以及如何通过增加新的散列算法来扩展.NET Framework。

第14 章对称加密

本章讨论了如何使用对称数据加密实现机密性,如何使用.NET Framework加密和解密数据。本章还阐述了如何通过增加新的对称加密算法来扩展.NET Framework。

第15 章非对称加密本章介绍了什么是非对称加密及其工作机制,阐述了非对称加密如何解决密钥交换的问题,示范了如何通过增加新的非对称加密算法来扩展.NET Framework。

第16 章数字签名

本章介绍了什么是数字签名及其工作机制,如何在.NET应用程序中使用数字签名。本章还示范了如何通过增加支持用户数字签名算法来扩展.NET Framework。

第17 章加密密钥

本章讨论了.NET Framework以何种方式支持密钥、密钥的重要性和如何创建密钥。

第IV部分:.NET应用程序框架

第18 章 ASP.NET 应用程序安全性

本章描述了使用何种功能增加ASP.NET应用程序的安全性,讨论了ASP.NET应用程序安全性的所有问题,.NET Framework 执行何种机制来提供验证、授权和为ASP.NET 应用程序提供服务。

第19 章 COM+ 安全

本章讨论了COM+ 安全服务和如何在COM+ 组件里运用安全服务。

第20 章事件日志服务

本章讨论了如何从.NET 应用程序中使用Windows 事件日志服务以检查Windows 安全事件。

第V部分:API快速参考

第V 部分全面地介绍了API 参考,它们涉及到的.NET Framework 基础类库中与安全有关的命名空间如下所示:

System.Security
System.Security.Cryptography
System.Security.Cryptography.X509Certificates
System.Security.Cryptography.Xml
System.Security.Permissions
System.Security.Policy
System.Security.Principal

本书读者对象

本书为两组人群而编写。第一组是.NET 应用程序的架构师和设计人员,他们必须懂得.NET 安全的作用和局限性以便将其运用到设计和计划中。第II、III 和IV 部分的每一章开始都详细讨论了这些技术,但是并没有详述各个类和方法。

第二组是C# 和 Visual Basic .NET 的程序员,他们想知道如何使用.NET Framework 的功能来编写更多的安全应用程序。在第II、III 和IV 部分每一章技术介绍之后,本书都详细阐述了如何编程运用.NET Framework 的功能;并列举了大量的代码实例来解释书中的观点。

本书建议

本书主要是针对.NET Framework 的安全编程;我们没有要求读者有先前对.NET 安全类库的接触,但是仍希望读者是具有基本经验的C# 或 Visual Basic .NET 程序员。

本书第V 部分讨论了.NET 应用程序的安全功能、Windows 平台的安全功能和其他外部服务之间的交互。在阅读这些章节时希望读者已熟悉外部技术,只需把注意力集中在安全编程方面。

本书约定

本书使用的字形惯例如下所示:斜体字(Italic):

          • 路径名、文件名和程序名

                        • Internet 地址,例如域名和URL

          • 本书定义的新名词

等宽字体(Constant Width):

          • 命令行,需要逐字输入的选项

          • 程序例子里的名字和关键字(包括方法名、变量名和类名)

等宽黑体(Constant Width Bold)

          • 编程代码里强调的部分

与我们联系

我们已尽最大努力修正了本书,但如果读者发现任何错误,请写信告诉我们:美国:

O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472

中国:100080 北京市海淀区知春路49 号希格玛公寓B 座809 室奥莱理软件(北京)有限公司

本书的网页上列出了勘误表、示例和任何额外的信息。可登录以下网址查询:

http://www.oreilly.com/catalog/prognetsec
http://www.oreilly.com.cn/book.php?bn=978-7-302-14500-4

如果想要发表关于本书的评论和技术问题,请发邮件至:

bookquestions@oreilly.com
info@mail.oreilly.com.cn

关于图书、会议、资源中心和O'Reilly 网络的更多信息,请查看我们的站点:

http://www.oreilly.com

http://www.oreilly.com.cn

参加本书翻译的人员有:靳京、陈宗斌、蔡京平、李毅、毕蓉蓉、祁海生、张贺乾、史宁、刘绿生、孙雷、蔡加双、安东辉、米翔 娟、刘颜、王宇宇、沈程亮、陆晓萍、金国良、俞群、李正智、赵敏、陈征、陈红霞、张景友、易小丽、陈婷、管学岗、王新彦、金惠敏、张海峰、徐晔、戴锋、张 德福、张士华、张锁玲、杜明宗、高玉琢、王涛、刘晓捷、董礼、何永利、李楠、陈宁、房金萍、黄骏衡、黄绪民、焦敬俭、李军、刘瑞东、潘曙光、蒲书箴、邵长 凯、郁琪琳、张广东、梁永翔、刘冀得、孙先国、张淑芝、张路红、程明、李大成、张春林、刘淑妮、侯经国、宫飞、高德杰、李振国、孙玲、申川。

.NET Framework 最重要的安全性是代码访问安全性,这在之前的章节中都讨论过。然而,有时也需要基于运行代码的用户来做出安全决定,它与代码本身的标识相对。为了达到这个 目的,.NET Framework 支持一种简单的基于角色的安全模型。本章解释了什么是基于角色的安全性,并讨论了.NET Framework 如何实现它。本章还描述了用于访问基于角色安全性的类,并示范了如何在程序中使用这些类。

解释基于角色的安全性

基于角色的安全性(RBS)是一种存在于同时计算环境中的公共安全模型。当用户希望访问一个计算机系统时,他们必须首先 证明自己的标识——该过程被称为验证。验证要求用户提供一组能够唯一标识其自己的凭据(credentials)。这些凭据通常是一个名称和密码,但是也 可能是一个物理存在的令牌,例如密码卡;或是一个生物属性,例如拇指纹。计算机系统使用机构(authority)来决定提供的凭据是否代表已知的用户和 该用户是否应该访问系统。在操作过程中,当执行授权(authorization)时——决定具有标识的用户可以访问那些操作和资源的过程,系统依赖于用 户的验证标识。用户个体的机构是根据角色来确定的。角色是一种逻辑分类,它能授予角色成员指定的权限。

介绍一个大家都应该熟悉的例子,Windows NT、Windows 2000 和Windows XP 的用户账户系统提供了一个RBS 的窗体。每个用户提供用户名和密码来验证Windows, 这些对Windows操作系统而言,用户账户代该组代表了用户的角色并决定用户可以访问的资源和操作。人们最常见的角色类型反映了每天生活中人们的工作与 活动,例如:出纳员、经理或管理员,并授权其成员执行相应的操作和访问相应的资源,而这些都是人们需要执行的日常职责。然而,角色是一个抽象的概念,可以 使用角色来代表任何一类被授予一组权限的组(例如,需要访问指定彩色打印机的某个项目的成员)。

解释.NET 基于角色的安全性

.NET 提供了一个总目标的RBS 抽象,可以结合各种验证和授权机制来使用该抽象。在.NET RBS中,标识代表了用户,此时运行的代码代表了该用户的利益。通常情况下,代表的是当前登录的Windows用户,但是也有例外。如果应用程序验证的是 与机构相对的用户,而不是Windows 账户用户,那么标识就可能不会代表当前登录的Windows 用户。

主体能够压缩包含标识和标识属于的角色;图10-1说明了这种关系。如果标识代表了一个Windows 用户账户,其角色就能够识别用户属于的Windows 组。因为主体压缩了标识和用户角色二者,所以主体是主要参考点,而.NET 运行库将基于角色的安全决定都以该主要参考点为基础。

图10-1:主体、标识和角色的关系

注意:.NET 基于角色的安全性是独立于COM+ 提供的基于角色安全性机制以外的。关于如何使用COM+ 安全,详情请见本书第19 章。

比较.NET 基于角色的安全性和Windows 安全性

.NET RBS 容易集成.NET Windows 用户账户系统,但是.NET RBS 不仅仅是Windows 用户安全在.NET 运行库的扩展。.NET RBS 和Windows 用户账户系统都是完全独立的安全机制,它们能同时操作并服务于不同的目的。

Windows的安全性通过控制用户访问重要操作系统的操作和资源来保护整个系统的完整性。每个Windows进程和线 程都具有一个相关访问令牌,该令牌代表了用户,而进程或线程是基于这些用户的利益运行的。访问令牌是一个操作系统对象,它含有用户权限和特权的信息,还包 含了用户属于的组。在Windows允许代码执行受保护的操作或访问受保护的资源之前,它会评估线程的访问令牌来保证该线程含有执行操作的必要权限。无论 一项操作是否被托管的.NET 应用程序或本地代码启动,Windows都会强制执行安全性。

.NET RBS 是在应用程序级别上操作,它提供了一种合适的机制来控制访问基于用户标识和角色的功能。例如,也许只允许用户(某些角色的成员)调用一种重要的方法或者根据当前用户的角色来可视化不同的菜单项。运行.NET 代码的每个线程都有一个与其相关的主体;该主体与Windows 访问令牌的目的是类似的,它允许.NET 决定运行线程代表的用户是否是指定角色的成员。尽管线程的主体和访问令牌都代表同一个用户,但它们是独立的对象而且它们之间没有固定的关系。.NET RBS 的使用时完全可选的;应该负责决定哪些是被.NET RBS 保护,还必须表示出代码的要求。

注意:.NET 应用程序能改变当前操作系统线程的Windows 访问令牌,这样就能模拟另一个Windows 用户——详情请见本章稍后的“模拟Windows 用户”一节。

编程基于角色的安全性

.NET运行库可以强制执行基于代码的安全性,它使用的技术和语法与第7章描述的技术和语法是类似的。安全要求指定了线程主体必须包含的标识或角色,在应用程序中发出基于角色的安全要求可以很好的保护功能。如果线程的主体不包含所需标识和角色,那么要求就会引发一个异常。

介绍Iidentity 和Iprincipal 接口

System.Security.Principal命名空间含有Iidentity和Iprincipal接口来代表 标识和主体。使用接口来代表标识和主体,.NET就会变得更灵活,也就是说它能相对容易地创建具体的基于角色安全性实现,从而支持许多不同的验证和授权机 制。.NET类库含有四种具体的RBS 实现,它们都使用Iidentity和Iprincipal接口:

Forms

提供一种基于角色的验证机制在ASP.NET 应用程序中使用。Forms 验证只提供Iidentity实现;本书第18 章将讨论Forms 验证。

Generic

提供一种通用的基于角色的安全性实现,它是独立于任何指定验证和授权机制的。详情请见“编程通用的基于角色安全性实现”一节。

Passport

提供一种基于角色的验证机制,它依赖于Microsoft Passport .NET 基于Web 的服务来验证用户。Passport 验证之提供Iidentity 实现。关于Passport 的讨论已超出本书范围,相关详细信息可参见.NET Framework SDK 和Passport SDK 文件。

Windows

提供一种基于角色的安全性实现,它是以Windows 用户账户系统定义的用户为基础的。详情请见“编程Windows 基于角色的安全性实现”一节。

表10-1 描述了Iidentity和Iprincipal接口的成员。在后续章节中,当讨论到这些接口的Windows 和Generic 实现时,本书会突出所有指定行为的实现。

表10-1:Iidentity 和Iprincipal 接口的成员

注意:Iprincipal接口不提供枚举或访问所有的主体角色。其唯一提供的访问是使用IsInRole 方法来测试个体角色名称,它与主体角色集是相对的。

决定当前主体

在.NET 运行库执行的每个线程都具有一个与其有关的Iprincipal对象。该线程的Iprincipal对象代表了一种用户,而运行的线程代表了该用户的利 益。其Iprincipal 对象还允许运行库为基于用户标识和角色的线程做出基于角色的安全性决定。然而,许多应用程序并不使用.NET的RBS功能,所以也就不能保护好其性能和资 源,.NET运行库不会自动分配Iprincipal对象到每个线程。如果想使用RBS,一旦有必要,则必须手动分配Iprincipal给一个线程,或 者通过配置运行库来自动创建一个Iprincipal。

可以使用System.AppDomain.SetThreadPrincipal方法来指定Iprincipal对 象,具体如下代码所示,运行库会自动分配该对象到应用程序域里运行的每个线程。每个应用程序域只能调用SetThreadPrincipal方法一次;否 则,就会抛出System.Security. Policy.PolicyException的一个实例:

# C#

public void SetThreadPrincipal(

 IPrincipal principal

);

# Visual Basic .NET

NotOverridable Public Sub SetThreadPrincipal( _

 ByVal principal As IPrincipal _

)

可以通过System.Threading.Thread.CurrentPrincipal属性来手动设置当前线程的 Iprincipal,从而不用使用SetThreadPrincipal来未整个应用程序域得到一个默认的Iprincipal。 CurrentPrincipal是static (C#)或Shared (Visual Basic .NET),它总是影响到当前执行线程的Iprincipal。也可以使用CurrentPrincipal来获得当前线程的Iprincipal。

注意:代码必须具有System.Security.Permissions.SecurityPermission 类的ControlPrincipal权限,从而调用AppDomain.SetThreadPrincipal方法或设置Thread.CurrentPrincipal属性。本书第7 章讨论过SecurityPermission类。

线程是在应用程序域的主体策略中运行,如果不使用AppDomain.SetThreadPrincipal 或Thread.CurrentPrincipal来分配Iprincipal到一个Thread,当代码试图获得线程的 CurrentPrincipal时,则由应用程序域的主体策略来决定发生什么情况。可以使用 System.AppDomain.SetPrincipalPolicy方法来配置应用程序域的主体策略,该方法具有以下签名:

# C#

public void SetPrincipalPolicy(
 PrincipalPolicy policy
);

# Visual Basic .NET

NotOverridable Public Sub SetPrincipalPolicy( _
 ByVal policy As PrincipalPolicy _
)

策略参数是System.Security.Principal.PrincipalPolicy枚举的一个成员,表10-2 列出了它们的值。所有应用程序域的默认主体策略是UnauthenticatedPrincipal,它创建了一个空的Iprincipal,这对做出基于角色的安全性决定并不起作用。

注意:要调用SetPrincipalPolicy,代码则必须具有System.Security.Permissions. SecurityPermission类的ControlPrincipal权限。

表10-2:PrincipalPolicy 枚举的成员

描述

NoPrincipal

不会创建主体或标识对象。获得Thread.CurrentPrincipal 属性将返回null (C#) or Nothing (Visual Basic .NET)。所有基于代码的安全要求都会失败

UnauthenticatedPrincipal

运行库创建了一个含有GenericIdentity 的Generic-Principal,设置其Name和AuthenticationType属性为空字符串(“”),设置其 IsAuthenticated 属性为false。GenericIdentity 不是任何角色的成员。本章后续将在“编程Windows基于角色安全性实现”一节讨论GenericIdentity和GenericPrincipal类

WindowsPrincipal

运行库创建了一个WindowsPrincipal,其含有的WindowsIdentity是基于当前线程的Windows 访问令牌的。WindowsPrincipal含有的角色是当前用户的Windows 组。本章后续将在“编程Windows 基于角色安全性实现”一节讨论


WindowsIdentity和WindowsPrincipal类

正如三种主体策 略选项所示,如果想要在不依赖于当前Windows用户的程序中使用一个RBS 实现,主体策略并不会起作用。应该确保当前线程具有正确的与其相关的Iprincipal。必须使用 AppDomain.SetThreadPrincipal 方法或Thread.CurrentPrincipal属性来创建Iprincipal对象并将其分配到线程。

编程Windows 基于角色的安全性实现

System.Security.Principal命名空间的WindowsIdentity和WindowsPrincipal类提供了一个RBS 实现,该实现允许将安全决定基于Windows 用户账户的标识和角色。WindowsIdentity类实现了Iidentity接口,并代表了Windows 用户账户。通过WindowsIdentity.Name 属性可以访问Windows 用户的名称,该名称具有“Domain\User”格式。

WindowsPrincipal类实现Iprincipal,并含有用户的WindowsIdentity对象和包含 用户的Windows 组的名称。任意内置的Windows 用户组的名称都具有“BUILTIN\”前缀——例如:“BUILTIN\Administrators ”或“BUILTIN\Users ”。当使用WindowsPrincipal.IsInRole方法来测试角色成员时记住这一点是很重要的。非内置组名称的前置是包含了组的域名;如果组 存在单个计算机上,该前置则是计算机名——例如:“MyDomain\Developers”或“MyMachine\Developers”。

注意:WindowsIdentity和WindowsPrincipal不能访问授予用户的Windows操作系统权限,也不能访问它们代表的组。RBS 决定必须完全取决于用户名和包含用户的组名。

WindowsIdentity类除了实现Iidentity接口定义的成员外还实现其他成员。这些附加成员为创建WindowsIdentity 对象提供了有用的功能,它们可以测试出WindowsIdentity对象代表了Windows 账户的哪种类型,还可以模拟Windows 用户。表10-3 总结了WindowsIdentity类的成员。

表10-3:WindowsIdentity 类的成员

属性

AuthenticationType

在Iidentity中定义。返回一个识别机制的字符串,可以使用该机制来验证WindowsIdentity对象代表的用户

IsAnonymous

如果WindowsIdentity对象代表的Windows账户类型是一个匿名账户,则返回“true”。通常情况下,在ASP.NET 应用程序中只能看到匿名属性;有关ASP.NET 安全的讨论,详情请见第18 章

IsAuthenticated

在Iidentity中定义。如果WindowsIdentity对象代表的用户被验证,则返回“true”;否则返回“false”

IsGuest 

如果WindowsIdentity对象代表的Windows账户类型是一个客户账户,则返回“true”

IsSystem

如果WindowsIdentity对象代表的Windows账户类型是一个系统账户,则返回“true”

Name

在Iidentity中定义。返回Windows 的用户登录名。Name 属性返回一个“Domain\User.”格式的字符串

表10-3:WindowsIdentity 类的成员(续)

成员Token

方法GetAnonymous

GetCurrent

Impersonate

描述返回System.IntPtr,它含有Windows 账户令牌的句柄,供WindowsIdentity对象代表的用户使用

一种静态方法。其返回一个WindowsIdentity对象,该对象代表一个匿名的Windows 用户账户

一种静态方法。其返回一个WindowsIdentity对象,该对象代表当前登录的Windows 用户账号

允许代码模拟其他的Windows 用户。详情请见“模拟Windows 用户”一节

WindowsPrincipal类只实现Iprincipal接口指定的基于角色的功能。Identity属性将包含 的WindowsIdentity作为一个Iidentity对象返回;因此,必须在使用表10-3 列出的附加方法之前将其导入WindowsIdentity。

现在大家都很熟悉IsInRole方法的实现过程了,该方法具有三种重载:

# C#

public virtual bool IsInRole(
 int rid

);

public virtual bool IsInRole(
 string role

);

public virtual bool IsInRole(
 WindowsBuiltInRole role

);

# Visual Basic .NET

Overridable Overloads Public Function IsInRole( _
 ByVal rid As Integer _

) As Boolean

Overridable Overloads Public Function IsInRole( _
 ByVal role As String _

) As Boolean

Overridable Overloads Public Function IsInRole( _
 ByVal role As WindowsBuiltInRole _

) As Boolean

在第一种重载中, rid参数指定了一个Windows 角色标识(RID)。RID 是Windows 组安全标识符的一个组件,它能提供方法以识别独立于语言本地化的组。第二种重载使用区分大小写的字符串,该字符串指定了测试对象的名称;这是 Iprincipal 定义的IsInRole 的标准格式。最后一种重载将许多System.Security.Principal. WindowsBuiltInRole枚举作为参数;WindowsBuiltInRole含有代表标准Windows 组的值,表10-4 为最常用的Windows 组成员比较了用于测试的角色名称、RID 和WindowsBuiltInRole值。

表10-4:通常使用的角色名称、RID 和WindowsBuiltInRole 成员

名称

RID (hex)

WindowsBuiltInRole

BUILTIN\Account Operators

0x224

AccountOperator

BUILTIN\Administrators

0x220

Administrator

BUILTIN\Backup Operators

0x227

BackupOperator

BUILTIN\Guests

0x222

Guest

BUILTIN\Power Users

0x223

PowerUser

BUILTIN\Print Operators

0x226

PrintOperator

BUILTIN\Replicator

0x228

Replicator

BUILTIN\Server Operators

0x225

SystemOperator

BUILTIN\Users

0x221

User

配置当前WindowsPrincipal

使当前主体代表活动Windows 用户的最简方法是调用System.AppDomain. SetPrincipalPolicy方法并将PrincipalPolicy.WindowsPrincipal值传递给它。首次代码会请求线程的 Iprincipal,运行库为基于当前活动的Windows 访问令牌的线程创建一个WindowsIdentity 和一个 WindowsPrincipal:

# C#

// Configure the current application domain's principal policy
// to represent the active Windows user
AppDomain.CurrentDomain.SetPrincipalPolicy(

 PrincipalPolicy.WindowsPrincipal);

# Visual Basic .NET

' Configure the current application domain's principal policy
' to represent the active Windows user
AppDomain.CurrentDomain.SetPrincipalPolicy( _
PrincipalPolicy.WindowsPrincipal)

也可以手动创建WindowsIdentity 和WindowsPrincipal 对象,并将AppDomain.SetThreadPrincipal方法或Thread.CurrentPrincipal属性传递给它们。 AppDomain.SetThreadPrincipal定义了一个默认主体,运行库会给该主体分配应用程序域里的任意线程,而 Thread.CurrentPrincipal属性则设置当前线程的主体。

WindowsIdentity.GetCurrent 方法返回一个代表当前Windows 用户的WindowsIdentity 对象。一旦具有WindowsIdentity 对象,就能将其传递给WindowsPrincipal构造函数并给当前进程分配新的WindowsPrincipal,具体如以下语句所示:

# C#

// Create a WindowsIdentity for the active Windows user
WindowsIdentity wi = WindowsIdentity.GetCurrent();

// Create a new WindowsPrincipal
WindowsPrincipal wp = new WindowsPrincipal(wi);

// Assign the WindowsPrincipal to the active thread.
Thread.CurrentPrincipal = wp;

# Visual Basic .NET

' Create a WindowsIdentity for the active Windows user
Dim wi As WindowsIdentity = WindowsIdentity.GetCurrent()

' Create a new WindowsPrincipal
Dim wp As WindowsPrincipal = New WindowsPrincipal(wi)

' Assign the WindowsPrincipal to the active thread.
Thread.CurrentPrincipal = wp

如果需要代表不 同主体的利益来使代码操作——名为“模拟”的进程,可以使用Thread.CurrentPrincipal属性或 AppDomain.SetThreadPrincipal方法来改变线程的当前主体。然而,改变当前主体并不会改变当前Windows 访问令牌。

模拟Windows 用户

有时,尽管当前登录的用户与之前登录的用户不同,但是也想让代码在操作系统级别上运行。这种情况在服务器应用程序中极其常见,该进程请求不同的用户并需要访问代表用户利益的资源,例如数据库。

WindowsIdentity类提供了一种机制来模拟另一个Windows 用户。然而,首先必须获得一个Windows 访问令牌,该令牌代表了被模拟的用户。遗憾的是当前的.NET Framework类库并没有能够托管访问Windows账户数据库的类,因此必须调用非托管的advapi32.dll Win32 库的LogonUser方法来获得访问令牌。LogonUser方法将用户名和密码作为其参数,还使用其他控制验证进程的机制,并为用户提供对访问令牌的 访问。如果LogonUser失败,还可以调用System.Runtime.InteropServices.Marshal. GetLastWin32Error方法来查出问题所在。GetLastWin32Error方法使用了advapi32.dll库的方法,并避免了从 CLR 执行内部调用到重写最后错误代码的WIN32 API 过程中可能引发的问题。

警 告:在Windows NT 和Windows 2000 操作系统中,运行程序的账户需要Windows SE_TCB_NAME特权来调用LogonUser以获得一个访问令牌。如果没有该特权,调用LogonUser就会失败,且调用 GetLastError会返回代码ERROR_PRIVILEGE_NOT_ HELD (value 1314)。为了授予一个账户SE_TCB_NAME 特权,必须配置本地安全策略来允许账户“作为操作系统的一部分运行”。该高度信任的特权允许账户创建代表了任意用户或权限集的访问令牌。应该只将权限授予 特别创建的账户来运行软件;千万不要将该权限授予用户账户。

一旦获得了访问令牌,就可以使用它来创建一个新的WindowsIdentity对象并调用其Impersonate方 法。Impersonate方法改变了当前线程的Windows访问令牌。在调用Impersonate之后,便可将任意操作系统作为模拟用户来操作。 Impersonate 方法返回一个System.Security.Principal.WindowsImpersonationContext 对象,该对象代表了优于模拟用户的Windows 用户。如果要还原原用户,则调用WindowsImpersonationContext.Undo方法。

例10-1 示范了如何模拟名为“Bob.”的Windows 用户。虽然示例说明了如何从托管代码中调用LogonUser,但是对非托管代码协同操作和LogonUser方法的完整介绍已超出本书的范围。关于调用非托管代码的详细信息请参阅.NET Framework SDK 文件,关于LogonUser 方法的详细信息请参阅Windows Platform SDK。

例10-1:模拟一个Windows 用户

# C#

using System;
using System.IO;
using System.Security.Principal;
using System.Security.Permissions;
using System.Runtime.InteropServices;

// Make sure we have permission to execute unmanged code
[assembly:SecurityPermission(SecurityAction.RequestMinimum,

 UnmanagedCode=true)]

public class WindowsImpersonationTest {

// Define the external LogonUser method from advapi32.dll.
 [DllImport("advapi32.dll", SetLastError=true)]
 static extern int LogonUser(String UserName, String Domain,

 String Password, int LogonType, int LogonProvider,
 ref IntPtr Token);

 public static void Main() {

 // Create a new initialized IntPtr to hold the access token
 // of the user to impersonate.
 IntPtr token = IntPtr.Zero;

 // Call LogonUser to obtain an access token for the user
 // "Bob" with the password "treasure". We authenticate against
 // the local accounts database by specifying a "." as the Domain
 // argument.
 int ret = LogonUser(@"Bob", ".", "treasure", 2, 0, ref token);

 // If the LogonUser return code is zero an error has occured.
 // Display it and exit.
 if (ret == 0) {

 Console.WriteLine("Error {0} occured in LogonUser",
 Marshal.GetLastWin32Error());
 return;
 }

 // Create a new WindowsIdentity from Bob's access token
 WindowsIdentity wi = new WindowsIdentity(token);

 // Impersonate Bob, saving a reference to the returned
 // WindowsImpersonationContext.
 WindowsImpersonationContext impctx = wi.Impersonate();

 // !!! Perform actions as Bob !!!
 // We create a file that Windows will show is owned by Bob.
 StreamWriter file = new StreamWriter("test.txt");
 file.WriteLine("Bob's test file.");
 file.Close();

 // Revert back to the original Windows user using the
 // WindowsImpersonationContext object.
 impctx.Undo();

}
}

# Visual Basic .NET

Imports System
Imports System.IO
Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Runtime.InteropServices

' Make sure we have permission to execute unmanged code

<assembly:SecurityPermission(SecurityAction.RequestMinimum, _
UnmanagedCode:=True)> _

Public Class WindowsImpersonationTest

' Define the external LogonUser method from advapi32.dll.
<DllImport("advapi32.dll", SetLastError := True)> _
Public Shared Function LogonUser(UserName As String, _
Domain As String, Password As String, LogonType As Integer, _
LogonProvider As Integer, ByRef Token As IntPtr) As Integer
End Function

 Public Shared Sub Main()

 ' Create a new initialized IntPtr to hold the access token
 ' of the user to impersonate.
 Dim token As IntPtr = IntPtr.Zero

 ' Call LogonUser to obtain an access token for the user
 ' "Bob" with the password "treasure". We authenticate against
 ' the local accounts database by specifying a "." as the Domain
 ' argument.
 Dim ret As Integer = LogonUser("Bob",".","treasure",2,0, token)

 ' If the LogonUser return code is zero an error has occured.
 ' Display it and exit.
 If ret = 0 Then

 Console.WriteLine("Error {0} occured in LogonUser", _
 Marshal.GetLastWin32Error())
 Return

 End If

 ' Create a new WindowsIdentity from Bob's access token
 Dim wi As WindowsIdentity = New WindowsIdentity(token)

 ' Impersonate Bob, saving a reference to the returned
 ' WindowsImpersonationContext.
 Dim impctx As WindowsImpersonationContext = wi.Impersonate()

 ' !!! Perform actions as Bob !!!
 ' We create a file that Windows will show is owned by Bob.
 Dim file As StreamWriter = New StreamWriter("test.txt")
 file.WriteLine("Bob's test file.")
 file.Close()

 ' Revert back to the original Windows user using the
 ' WindowsImpersonationContext object.
 impctx.Undo()

 End Sub
End Class

发出基于角色的安全性要求

基于角色的安全要求的功能与代码访问安全要求相似。关键的不同之处在于基于角色的安全要求从不会引发堆栈步。基于角色的 安全要求的结果完全取决于活动线程主体的标识和角色。如果线程主体并没含有要求的标识或角色,运行库便会引发一个 System.Security.SecurityException。如果主体符合要求,便可顺利执行。

PrincipalPermission类及其名为PrincipalPermissionAttribute的属性副 本提供了一种机制来调用程序里基于角色的安全要求。PrincipalPermission类允许使用强制性安全语法,而 PrincipalPermissionAttribute类允许使用声明性语法;两个类都是System.Security.Permissions 命名空间的成员。本书第7章讨论了强制性和声明性两种安全要求的语法和使用,因此在以下章节,本书只对它们作大概的介绍。

注意:可以通过权限和属性来唯一强制执行代码访问安全性,与这一点不同,通常通过直接调用线程主体的成员和标识对象来作出基于角色的安全决定。如果RBS 实现不仅包括IIdentity或Iprincipal接口定义的功能而且还包括其他功能,这一点就尤其有用。例如,实现可以将指定功能赋予基础验证或授权的机制,这和“编程Windows 基于角色的安全实现”一节讨论的WindowsIdentity对象类似。

使用强制性基于角色的安全性语句

为了发出一个强制性的RBS要求,必须例示一个PrincipalPermission对象并调用其Demand方法。最常用的PrincipalPermission构造函数具有以下签名:

# C#

public PrincipalPermission(

 string name,

 string role

);

# Visual Basic .NET

Public Sub New( _

 ByVal name As String, _

 ByVal role As String _

)

名称和角色参数指定了一些值,当前主体必须具有这些值才能使Demand 成功。每个PrincipalPermission只能指定一个单角色名称。当前主体必须与指定的名称和角色值都相匹配;然而,也能为每个参数指定null (C#) 或Nothing (Visual Basic .NET) 从而匹配任何值。表10-5 解释了PrincipalPermission如何使用这些参数来决定Demand的输出结果。

表10-5:PrincipalPermission 构造函数的参数

name必须与Thread.CurrentPrincipal.Identity.Name 属性的值相匹配。GenericPrincipal和WindowsPrincipal两个类都能对Name 属性作区分大小写的比较

role是Thread.CurrentPrincipal.Iprincipal.IsInRole方法的一个参数, 用以测试当前主体是否是指定角色的成员。GenericPrincipal和WindowsPrincipal两个类都能对IsInRole作区分大小写 的比较

以下语句示范了如何使用PrincipalPermission类来调用多种强制性的RBS要求:

# C#

// Demand that the active principal has the identity name "Peter".
PrincipalPermission p1 = new PrincipalPermission("Peter", null);
p1.Demand();

// Demand that the active principal is a member of the "Developers"
// group with any identity.
PrincipalPermission p2 = new PrincipalPermission(null,"Developers");
p2.Demand();

// Demand that the active principal have the identity name "Bart"
// and be a member of the "Managers" group.
PrincipalPermission p3 = new PrincipalPermission("Bart", "Managers");
p3.Demand();

# Visual Basic .NET

' Demand that the active principal has the identity name "Peter".
Dim p1 As PrincipalPermission = _
New PrincipalPermission("Peter",Nothing)
p1.Demand()

' Demand that the active principal is a member of the "Developers"
' group with any identity.
Dim p2 As PrincipalPermission = _
New PrincipalPermission(Nothing,"Developers")
p2.Demand()

' Demand that the active principal have the identity name "Bart"
' and be a member of the "Managers" group.
Dim p3 As PrincipalPermission = _
New PrincipalPermission("Bart","Managers")
p3.Demand()

PrincipalPermission实现了System.Security.Ipermission接口,并定义了 Copy、Intersect、IsSubsetOf和Union方法来操作PrincipalPermission对象。 PrincipalPermission对象的组成意味着Intersect和IsSubSetOf方法几乎不起作用; PrincipalPermission主要实现它们来满足Ipermission接口的要求。然而,Union方法确实提供了有用的功能,它能够发出安 全要求来测试名称/角色的多重组。

两个 PrincipalPermission对象的Union 是一个PrincipalPermission对象,该对象包括了两个源对象的名称和角色元素。例如,如果这两个源 PrincipalPermission对象含有名称/ 角色的值“Alice”/“managers”和“Bob”/“developers”,其并集则会含有这两对名称/ 角色。如果当前主体匹配这两对中的任何一对,在结果PrincipalPermission上调用Demand就会成功。然而, PrincipalPermission会独立地保持和比较包含的名称/角色对。将并集和主体的名称/角色对/

“Alice”“developers”作比较会失败的。如果要理解Union 生成的PrincipalPermission的内容,请参见PrincipalPermission. ToString方法的以下输出:

<Permission class="System.Security.Permissions.PrincipalPermission,

mscorlib, Version=1.0.5000.0, Culture=neutral,

PublicKeyToken=b77a5c561934e089"

 version="1"> <Identity Authenticated="true"

 ID="Alice"

 Role="managers"/>

 <Identity Authenticated="true"

 ID="Bob"

 Role="developers"/>

</Permission>

使用声明性基于角色的安全性语句

可以将PrincipalPermissionAttribute应用于类、方法、属性或事件来强制执行声明性安全要 求。PrincipalPermissionAttribute与权限属性(见第7章)的关键不同之处在于不能在程序集级别上应用 PrincipalPermisisonAttribute。这意味着不能发出基于角色的权限请求,例如:RequestMinimum、 RequestOptional 和RequestRefuse。另外,因为RBS 不使用或影响堆栈的调用,所以就没有可用的声明性堆栈重写语句。因此,只能使用声明性语法(Demand 、LinkDemand 和InheritenceDemand)来调用以下三种基于角色的安全语句。

PrincipalPermissionAttribute类定义了Name和Role属性,可以使用这些属性来指定名 称和角色值,而为了使声明性的RBS要求成功,当前主体必须具有这些名称和角色值。以下语句示范了等价的 PrincipalPermissionAttribute语法,可以使用它来发出与之前章节所介绍的一样的要求:

# C#

// Demand that the active principal has the identity name "Peter".
[PrincipalPermission(SecurityAction.Demand, Name = "Peter")]

// Demand that the active principal is a member of the "Developers"
// group with any identity.
[PrincipalPermission(SecurityAction.Demand, Role = "Developers")]

// Demand that the active principal have the identity name "Bart"
// and be a member of the "Managers" group.
[PrincipalPermission(SecurityAction.Demand, Name = "Bart", Role = "Managers")]

# Visual Basic .NET

' Demand that the active principal has the identity name "Peter".
<PrincipalPermission(SecurityAction.Demand, Name := "Peter")> _

' Demand that the active principal is a member of the "Developers"
' group with any identity.
<PrincipalPermission(SecurityAction.Demand, Role := "Developers")> _

' Demand that the active principal have the identity name "Bart"
' and be a member of the "Managers" group.
<PrincipalPermission(SecurityAction.Demand, Name:="Bart", Role:="Managers")> _

编程通用的基于角色的安全性实现

如果依赖机构而不是Windows 账户系统来验证和授权用户,系统的制造者可能会提供Iidentity和Iprincipal实现,它们能直接与机构一块工作。如果不是依赖于机构,最简 单的方法则是从用户机构处获得标识和角色信息,并使用System.Security.Principal命名空间的GenericIdentity和 GenericPrincipal类来启动托管应用程序的基于角色的安全支持。GenericIdentity和GenericPrincipal 类提供一种通用的RBS 实现,可以独立使用Windows 用户账户数据库的RBS 实现。GenericIdentity类实现了Iidentity,而GenericPrincipal类实现了Iprincipal。

配置当前GenericPrincipal

设置当前主体到GenericPrincipal对象的技术比之前“配置当前WindowsPrincipal”一节描述的设置技术更有限。一个应用程序域的主体策略不可能自动创建有用的GenericPrincipal对象;因此,可以有以下两项选择:

          •创建一个GenericPrincipal对象并将其传递给AppDomain.SetThreadPrincipal 方法。这定义了默认主体,运行库会将该主体分配给在应用程序域里执行的任意线程。

          •创建一个GenericPrincipal对象并使用它来设置Thread.CurrentPrincipal属性。这设置了当前线程的主体。

要创建一个GenericPrincipal,必须首先创建一个GenericIdentity。GenericIdentity 类提供了具有以下签名的两种构造函数:

# C#

public GenericIdentity(
 string name
);

public GenericIdentity(
 string name,
 string type

);

# Visual Basic .NET
Public Sub New( _

 ByVal name As String _
)
Public Sub New( _

 ByVal name As String, _
 ByVal type As String _
)

第一种构造函数使用单个的name参数,该参数指定了标识代表的用户名称。除了使用name参数,第二种构造函数还使用 了type参数。type参数允许指定能够识别验证机制类型的字符串,该机制用于验证用户。一旦name 和type参数只含有有效字符串,GenericIdentity类就会强行使该name和type参数的格式或内容没有限制。这会提供最大程度的灵活 性,它允许将GenericIdentity类和任意用户验证机制一并使用。以下语句示范了如何使用GenericIdentity构造函数:

# C#

GenericIdentity i1 = new GenericIdentity("Peter");
GenericIdentity i2 = new GenericIdentity("Peter","SmartCard");
# Visual Basic .NET
Dim i1 As GenericIdentity = New GenericIdentity("Peter")

Dim i2 As GenericIdentity = New GenericIdentity("Peter","SmartCard")

GenericPrincipal类提供了一个构造函数,使用Iidentity对象和一个字符串数组作为参数:

# C#

public GenericPrincipal(
 IIdentity identity,
 string[] roles

);

# Visual Basic .NET

Public Sub New( _

 ByVal identity As IIdentity, _
 ByVal roles() As String _
)

identity参数可以具有实现Iidentity和代表GenericPrincipal应该代表的用户的任意类 型。roles参数含有包括user 的role 名称组的字符串数组。必须在Generic-Principal对象构造函数中指定用户角色,因为GenericPrincipal类是对立于任意基础用 户授权机制的。以下语句示范了如何从GenericIdentity创建一个Generic-Principal,如何赋予当前主体使用的新 GenericPrincipal 以Thread. CurrentPrincipal属性:

# C#

// Create a GenericIdentity for the user Peter
GenericIdentity gi = new GenericIdentity("Peter");
// Create a GenericPrincipal for Peter and specify membership of the

// Developers and Managers roles
String[] roles = new String[]{"Developers", "Managers"};
GenericPrincipal gp = new GenericPrincipal(gi, roles);

// Assign the new principal to the current thread
Thread.CurrentPrincipal = gp;

# Visual Basic .NET
' Create a GenericIdentity for the user Peter
Dim gi As GenericIdentity = New GenericIdentity("Peter")

' Create a GenericPrincipal for Peter and specify membership of the
' Developers and Managers roles
Dim roles() As String = New String() {"Developers", "Managers"}
Dim gp As GenericPrincipal = New GenericPrincipal(gi,roles)

' Assign the new principal to the current thread
Thread.CurrentPrincipal = gp;

GenericIdentity和 GenericPrincipal两类都比较简单,除了其Iidentity和Iprincipal接口定义的基于角色的功能之外,它们不能提供其他基于 角色的功能。表10-6描述了Iidentity和Iprincipal接口的GenericIdentity和GenericPrincipal 类实现。

表10-6:GenericIdentity 和GenericPrincipal 类的成员

成员

描述

GenericIdentity 类


AuthenticationType

只读属性。它能得到在GenericIdentity构造函数的type参数


中指定的验证类型。如果没有指定type,则会返回一个空字符串

表10-6:GenericIdentity 和GenericPrincipal 类的成员(续)



IsAuthenticated

只读属性。一旦GenericIdentity构造函数的name参数指定的用户名不是空字符串,则会返回“true”;否则,会返回“false”

Name

只读属性。它能得到在GenericIdentity构造函数的name参数中指定的用户名

GenericPrincipal 类

Identity

只读属性。它能得到在GenericPrincipal构造函数的identity 参数中指定的Iidentity对象。必须赋予其正确类型,通常是赋予GenericIdentity

IsInRole

一种方法。它使用含有角色名称的字符串参数,如果指定的角色名称与GenericPrincipal构造函数的roles参数含有的名称匹配,则返回“true”


数字签名提供了一套解决消息验证问题的方案,它能使Bob正确识别他收到的消息的作者。例如,Bob 使用数字签名就能区别出Alice 发送的消息和Eve 伪造的消息。本章解释了什么是数字签名,它们如何工作及如何使用它们。本章还添加了对数字签名的支持来实现第15 章介绍的ElGamal 算法,这样就能扩展.NET Framework。

解释数字签名

数字签名是本书第15章讨论的非对称算法的又一个应用。可以使 用非对称密钥将签名函数添加到非对称算法,从而为消息创建一个“签名”。接收方能够使用确认函数来验证签名。非对称算法支持签名和确认函数,它们是一种数 字签名算法。每个个体消息或文件的数字签名是特定的,如果Alice 签名了一条消息,Bob 就能相信Eve 没有给他发送一条伪造的消息。

图16-1 概述了如何使用数字签名,其工作机制如下所示:

图16-1:使用非对称签名算法来提供消息认证

应该注意:消息的发送者Alice 创建了密钥对并保留了私钥;这和非对称加密不同,在非对称加密中是接收方(Bob)负责创建密钥。数字签名一个消息能创建一个隔离的数据块,Alice 会将该数据块和消息一并发送给Bob。

非对称算法运行起来相对较慢,这意味着Alice 不用签名她所发送的整个消息;她可以为消息创建一个加密的散列码并用其来签名代替。给消息签名散列码会受到散列码安全(本书第13 章讨论过)的限制,但是签名散列码是等价于签名消息本身的,而且要快得多,因为要处理的散列码数据比非对称算法的要少。图16-2 说明了Alice 使用私钥、数据散列码和非对称签名函数来创建签名的方法。

Bob 通过创建他自己的散列码并使用非对称签名验证和Alice 的公钥来验证签名。如果以任何方式改变了消息,Bob 就不能验证签名,这意味着Eve 不能背地里篡改内容,并且Bob 不能改变消息,并在以后声称Alice 签署了改变的版本。Alice 和Bob 必须使用相同的散列码算法;否则,Bob 将不能验证有效的签名。图16-3 说明了Bob 如何验证一个签名。

数字签名允许多个人给一个消息或文件签名,例如一份合同。每个人都使用基础协议来签名消息,并将他们的签名发送给其他所有的签名者。如果Alice 和Bob 想要签名一份数字合同,他们使用的基础协议如下所示:

图16-2:将散列码作为创建一个数字签名的基础来使用

图16-3:将散列码作为确认一个数字签名的基础来使用

1         Alice 和Bob 在要签名的合同内容上达成一致。

2         Alice 为合同创建一个散列码,并使用她的私钥为其签名。

3         Bob 为合同创建一个散列码,并使用他的私钥为其签名。

4         Alice 和Bob 交换数字签名和公钥。

5         Alice检查以确认Bob使用了正确的散列码(之后再为期望的合同版本签名),并使用Bob 的公钥来验证签名。

6         Bob 检查以确认Alice 使用了正确的散列码并使用Alice 的公钥来验证签名。

如果出现任何争议,第三方(可能是一位法官或其他仲裁者)能够使用Alice 和Bob 的公钥来验证确认Alice 和Bob 签名了同样的合同。任何一方都不能改变合同(如果那样的话会导致散列码不匹配),也不能否认他们签名了合同(因为第三方能验证每个分开的签名)。

数字签名安全

数字签名有两个目标:它们必须具有加密安全性,而且它们还必须为手写签名或打印文件提供相同的安全功能。关于数字签名的安全功能总结如下:

Eve 不能够伪造Alice 的数字签名。除非知道Alice 的密钥,否则Eve 不能创建一个假的签名;和绝大多数的加密一样,数字签名也依赖于对密钥的仔细保护和管理。如果能够伪造Alice 的签名,Eve 就能模仿Alice 签署任何文件。

Eve 不能重新使用Alice 的数字签名。Eve 只能从文件中获得Alice 的签名,如果另一个文件和该文件能生成同样的数据散列码,则会将该签名和另一个文件联系起来。本书第13 章详细说明了为什么这一点不能轻易做到。

Eve 不能改变签名了的文件。如果Eve 改变了文件的内容,那么Alice 的签名就不再有效,因为改变后的文件会为Alice 签名的文件生成一个不同的数据散列码。如果另一个文件和源文件的数据散列码相同,Eve则可能会成功修改这另一个文件(实际上就是重新使用了Alice 的签名)。

Alice 不能否认她签名了文件。Alice 不能签名一个文件,之后也会声明她没有签名,因为本书假设Alice 是唯一知道她自己密钥的人。如果她怀疑自己的密钥受到威胁,那么就应该创建一个新密钥对。

从这些功能可以看出:数字签名能够提供一种和物理签名类似的合理机制。还可以发现,整个数字签名的安全是基于以下假设的:

编程数字签名

.NET Framework将加密和数字签名算法组合到一起,并将它作为AsymmetricAlgorithm 类的子类。图16-4 描述了数字签名算法的.NET 类层次结构,它和加密算法层次结构的唯一不同点在于:数字签名算法添加了一个只有DSA 支持的签名。

抽象算法类(RSA 和DSA)及其实现的对应方法(RSACryptoServiceProvider 和DSACryptoServiceProvider)之间缺乏总的一致性,这意味着应该有几种等价的方法来完成签名操作,以下几节会详细示范这一点。

使用抽象类

抽象的System.Security.Cryptography.DSA类定义了CreateSignature方法,该

方法接收一个SHA-1 散列码,PKCS #1 会格式化并签名该代码,具体如以下范例所示(本书省略了指定密钥对的过程):

图16-4:数字签名算法的.NET Framework 类层次结构

# C#

// create the plaintext
byte[] x_plaintext = Encoding.Default.GetBytes("Programming .NET Security");

// create the SHA-1 algorithm instance and create a hash code for the plaintext
SHA1 x_sha = SHA1.Create();
byte[] x_hashcode = x_sha.ComputeHash(x_plaintext);

// create an instance of the DSA algorithm using
// the Create method in the abstract class
DSA x_dsa = DSA.Create();

// use the CreateSignature method to sign the
// SHA-1 hashcode created from the plaintext
byte[] x_signature = x_dsa.CreateSignature(x_hashcode);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte() = Encoding.Default.GetBytes("Programming .NET Security")

' create the SHA-1 algorithm instance and create a hash code for the plaintext
Dim x_sha As SHA1 = SHA1.Create()
Dim x_hashcode As Byte() = x_sha.ComputeHash(x_plaintext)

' create an instance of the DSA algorithm using
' the Create method in the abstract class
Dim x_dsa As DSA = DSA.Create()

' use the CreateSignature method to sign the
' SHA-1 hashcode created from the plaintext
Dim x_signature As Byte() = x_dsa.CreateSignature(x_hashcode)

在使用CreateSignature方法时,必须自己创建SHA-1 散列码。该方法会返回用字节数组表示的DSA 签名。

注意:DSA签名函数是依赖于随机数字来创建签名的。这意味着任何两个签名都是不同的,即使为同样的数据创建签名或者使用相同的密钥对,其签名也是不一样的。

VerifySignature方法是CreateSignature的对应方法,VerifySignature接受字符数组表示的SHA-1 散列码和签名来验证。以下语句示范了如何验证一个DSA 签名:

# C#

// create the plaintext
byte[] x_plaintext
 = Encoding.Default.GetBytes("Programming .NET Security");

// define the signature to verify
byte[] x_signature = new Byte[] {0x7D, 0x2B, 0xD7, 0x3D, 0x88, 0xCB, 0x1B, 0x6B,

0x04, 0x62, 0x95, 0xBE, 0x28, 0x59, 0x3E, 0xC5,
 0x40, 0xDA, 0x79, 0xFE, 0x3B, 0x25, 0x08, 0x4B,
 0x27, 0xF1, 0x31, 0x2A, 0x6F, 0x7C, 0x6E, 0x35,
 0x45, 0x9A, 0x49, 0x4C, 0xA4, 0x5E, 0xE6, 0xA0};

// create the SHA-1 algorithm instance and
// create a hash code for the plaintext
SHA1 x_sha = SHA1.Create();
byte[] x_hashcode = x_sha.ComputeHash(x_plaintext);

// create an instance of the DSA algorithm using
// the Create method in the abstract class
DSA x_dsa = DSA.Create();

// use the VerifySignature method to verify the DSA signature
bool x_signature_valid = x_dsa.VerifySignature(x_hashcode, x_signature);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte()= Encoding.Default.GetBytes("Programming .NET Security")

' define the signature to verify

Dim x_signature As Byte() = New Byte() {&H7D, &H2B, &HD7, &H3D, &H88, &HCB, _
 &H1B, &H6B, &H4, &H62, &H95, &HBE, &H28, _
 &H59, &H3E, &HC5, &H40, &HDA, &H79, &HFE, _
 &H3B, &H25, &H8, &H4B, &H27, &HF1, &H31, _
 &H2A, &H6F, &H7C, &H6E, &H35, &H45, &H9A, _
 &H49, &H4C, &HA4, &H5E, &HE6, &HA0}

' create the SHA-1 algorithm instance and
' create a hash code for the plaintext
Dim x_sha As SHA1 = SHA1.Create()
Dim x_hashcode As Byte() = x_sha.ComputeHash(x_plaintext)

' create an instance of the DSA algorithm using
' the Create method in the abstract class
Dim x_dsa As DSA = DSA.Create()

' use the VerifySignature method to verify the DSA signature
Dim x_signature_valid As Boolean = x_dsa.VerifySignature(x_hashcode,
x_signature)

如果验证了签名,VerifySignature方法则返回“true”,如果签名无效,则返回“false”。

注意:抽象的RSA 类不提供任何方法来支持RSA 算法的数字签名。

使用实现类

RSACryptoServiceProvider 和DSACryptoServiceProvider类都定义了四种与数字签名有关的方法(除了CreateSignature和 VerifySignature方法之外,DSACrypto-ServiceProvider实现了这些方法)。表16-2 总结了这些方法。

表16-2:算法实现签名的方法

方法

描述

SignData

从源文件创建一个数字签名

SignHash

从散列码创建一个数字签名

VerifyData

验证一个与源文件对应的数字签名

VerifyHash

验证一个与源文件对应的散列码

SignData方法通过生成一个散列码来创建一个签名,使用PKCS #1 来格式化散列码,并为其结果签名。相应的VerifyData方法创建了一个PKCS #1——格式化的散列码,并使用它来验证该签名。

就RSA算法而 言,它是使用System.Security.Cryptography.HashAlgorithm实例来生成的散列码,可以将该实例作为参数提供给 SignData和VerifyData方法;关于HashAlgorithm类,详情请见第13 章。DSA 算法通常使用SHA-1 散列算法来生成散列码。

以下语句示范了如何使用SignData 方法来为字节数组创建签名,以及如何使用VerifyData方法来验证签名:

# C#
// create the plaintext
byte[] x_plaintext = Encoding.Default.GetBytes("Programming .NET Security");

// create an instance of the DSA implementation class

DSACryptoServiceProvider x_dsa = new DSACryptoServiceProvider();
// create a signature for the plaintext
byte[] x_dsa_signature = x_dsa.SignData(x_plaintext);
// verify the signature, using the plaintext
bool x_dsa_sig_valid = x_dsa.VerifyData(x_plaintext, x_dsa_signature);

// create an instance of the RSA implementation class
RSACryptoServiceProvider x_rsa = new RSACryptoServiceProvider();
// create an instance of the SHA-1 hashing algorithm
HashAlgorithm x_sha1 = HashAlgorithm.Create("SHA1");
byte[] x_rsa_signature = x_rsa.SignData(x_plaintext, x_sha1);
// verify the signature, using the plaintext
bool x_rsa_sig_valid = x_rsa.VerifyData(x_plaintext, x_sha1,
x_rsa_signature);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte() = Encoding.Default.GetBytes("Programming .NET Security")

' create an instance of the DSA implementation class
Dim x_dsa As DSACryptoServiceProvider = New DSACryptoServiceProvider()
' create a signature for the plaintext
Dim x_dsa_signature As Byte() = x_dsa.SignData(x_plaintext)
' verify the signature, using the plaintext
Dim x_dsa_sig_valid As Boolean = x_dsa.VerifyData(x_plaintext, x_dsa_signature)

' create an instance of the RSA implementation class
Dim x_rsa As RSACryptoServiceProvider = New RSACryptoServiceProvider()
' create an instance of the SHA-1 hashing algorithm
Dim x_sha1 As HashAlgorithm = HashAlgorithm.Create("SHA1")
Dim x_rsa_signature As Byte() = x_rsa.SignData(x_plaintext, x_sha1)
' verify the signature, using the plaintext
Dim x_rsa_sig_valid As Boolean = x_rsa.VerifyData(x_plaintext, x_sha1, _
x_rsa_signature)

使用SignData方法的主要优势在于:可以从一个数据流中 读出待签的数据,这对签名文件也同样适用(它是要读入系统内存)。以下语句示范了如何通过读入流数据并使用SignData 方法来创建一个DSA。就该示例而言,假设要签名的是一个名为mydocument.txt 的磁盘文件:

# C#

// open the file as a stream
System.IO.FileStream x_stream
 = new System.IO.FileStream("mydocument.txt", System.IO.FileMode.Open);

// create an instance of the DSA implementation class
DSACryptoServiceProvider x_dsa = new DSACryptoServiceProvider();

// create a signature using the stream
byte[] x_dsa_signature = x_dsa.SignData(x_stream);

// close the stream
x_stream.Close();

# Visual Basic .NET
' open the file as a stream
Dim x_stream As System.IO.FileStream _
= New System.IO.FileStream("mydocument.txt", System.IO.FileMode.Open)

' create an instance of the DSA implementation class

Dim x_dsa As DSACryptoServiceProvider = New DSACryptoServiceProvider()
' create a signature using the stream
Dim x_dsa_signature As Byte() = x_dsa.SignData(x_stream)

' close the stream
x_stream.Close()

SignHash和VerifyHash方法格式化并签名了一 个已存在的散列码,将该散列码作为参数传递到这些方法中;散列算法是用于创建散列码的,而这些方法也要求一个能代表散列算法标识的字符串参数(当签名含有 正确的PKCS #1算法ID时,情况也是这样)。CryptoConfig类的静态MapNameToOID方法会返回散列算法的ID,以便以下语句获得SHA-1 算法的ID:

# C#

string x_id = CryptoConfig.MapNameToOID("SHA1");
Console.WriteLine(x_id);
# Visual Basic .NET

Dim x_id As String = CryptoConfig.MapNameToOID("SHA1")
Console.WriteLine(x_id)

这些语句生成了以下输出,它们是表16-1 列出的部分ID 的十进制表示形式:

1.3.14.3.2.26

以下语句示范了如何使用SignHash和VerifyHash方法及DSA 算法来创建并验证一个签名。应该注意:即使DSA 说明书需要使用SHA-1,DSA实现也会要求散列算法的ID:

# C#

// create the plaintext
byte[] x_plaintext = Encoding.Default.GetBytes("Programming .NET Security");
// create a hash code for the plaintext, using the SHA-1 algorithm

byte[] x_hashcode = HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext);

// create an instance of the DSA implementation class
DSACryptoServiceProvider x_dsa = new DSACryptoServiceProvider();
// create a DSA signature using the hash code

byte[] x_signature = x_dsa.SignHash(x_hashcode,
CryptoConfig.MapNameToOID("SHA1"));

// verify the signature
bool x_sig_valid = x_dsa.VerifyHash(x_hashcode, CryptoConfig.MapNameToOID("SHA1"),
 x_signature);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte() = Encoding.Default.GetBytes("Programming .NET Security")

' create a hash code for the plaintext, using the SHA-1 algorithm
Dim x_hashcode As Byte() =
HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext)

' create an instance of the DSA implementation class
Dim x_dsa As DSACryptoServiceProvider = New DSACryptoServiceProvider()

' create a DSA signature using the hash code
Dim x_signature As Byte() = x_dsa.SignHash(x_hashcode, _
CryptoConfig.MapNameToOID("SHA1"))

' verify the signature
Dim x_sig_valid As Boolean = x_dsa.VerifyHash(x_hashcode, _
CryptoConfig.MapNameToOID("SHA1"), x_signature)

使用签名格式化程序类

.NET Framework 指定了分开的格式化程序类和反格式化程序类,具体如图16-5 的层次结构所示。这些类使用PKCS #1 来格式化散列码,并将结果签名来协助抽象类和实现类。这些类提供的功能与使用之前章节描述的方法是等价的;使用格式化程序类和反格式化程序类并没有特别的 优势。

图16-5:.NET Framework 的签名格式化程序类层次结构

抽象的AsymmetricSignatureFormatter类定义了表16-3 描述的方法。抽象类不会指定如何格式化一个散列码,而实现类则能够定义任何适用于算法的格式化;这两个默认的.NET 实现类都使用PKCS #1 格式化。

表16-3:AsymmetricSignatureFormatter 成员

SetHashAlgorithm 指定了散列算法的名称,该算法是用于创建待签散列码的;在PKCS #1 格式化中,签名后的数据包含了算法的ID SetKey 指定了一个非对称签名算法的实例,该算法是用于创建数字签名的CreateSignature 从散列码创建一个数字签名

以下语句示范了如何使用RSAPKCS1SignatureFormatter类来创建一个数字签名:

# C#

// create the plaintext
byte[] x_plaintext = Encoding.Default.GetBytes("Programming .NET Security");

// create a hash code for the plaintext, using the SHA-1 algorithm
byte[] x_hashcode = HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext);

// create an instance of the DSA implementation class
DSACryptoServiceProvider x_dsa = new DSACryptoServiceProvider();

// create the signature formatter
DSASignatureFormatter x_formatter = new DSASignatureFormatter();
// set the instance of the DSA algorithm that will sign the data
x_formatter.SetKey(x_dsa);
// set the name of the hashing algorithm we used to create the hash code
x_formatter.SetHashAlgorithm("SHA1");
// create the formatted DSA signature
byte[] x_signature = x_formatter.CreateSignature(x_hashcode);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte() = Encoding.Default.GetBytes("Programming .NET Security")

' create a hash code for the plaintext, using the SHA-1 algorithm
Dim x_hashcode As Byte() =
HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext)

' create an instance of the DSA implementation class
Dim x_dsa As DSACryptoServiceProvider = New DSACryptoServiceProvider()

' create the signature formatter
Dim x_formatter As DSASignatureFormatter = New DSASignatureFormatter()
' set the instance of the DSA algorithm that will sign the data
x_formatter.SetKey(x_dsa)

' set the name of the hashing algorithm we used to create the hash code
x_formatter.SetHashAlgorithm("SHA1")
' create the formatted DSA signature
Dim x_signature As Byte() = x_formatter.CreateSignature(x_hashcode)

抽象的 AsymmetricSignatureDeformatter是验证数字签名的基础,而这些数字签名是由 AsymmetricSignatureFormatter类的子类创建的。表16-4 总结了Asymmetric-SignatureDeformatter类的方法。

表16-4:AsymmetricSignatureDeformatter 成员

方法

描述

SetHashAlgorithm

指定用于验证签名的散列码的名称

SetKey

指定用于验证数字签名的非对称签名算法的一个实例

VerifySignature

验证一个数字签名

以下语句示范了如何使用DSASignatureDeformatter类来验证数字签名:

# C#

// create the plaintext
byte[] x_plaintext = Encoding.Default.GetBytes("Programming .NET Security");

// create a hash code for the plaintext, using the SHA-1 algorithm
byte[] x_hashcode = HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext);

// define the signature to verify

byte[] x_signature = new Byte[] {0x7D, 0x2B, 0xD7, 0x3D, 0x88, 0xCB, 0x1B, 0x6B,
 0x04, 0x62, 0x95, 0xBE, 0x28, 0x59, 0x3E, 0xC5,
 0x40, 0xDA, 0x79, 0xFE, 0x3B, 0x25, 0x08, 0x4B,
 0x27, 0xF1, 0x31, 0x2A, 0x6F, 0x7C, 0x6E, 0x35,
 0x45, 0x9A, 0x49, 0x4C, 0xA4, 0x5E, 0xE6, 0xA0};

// create an instance of the DSA implementation class
DSACryptoServiceProvider x_dsa = new DSACryptoServiceProvider();

// create the signature deformatter
DSASignatureDeformatter x_deformatter = new DSASignatureDeformatter();
// set the instance of the DSA algorithm that will verify the signature
x_deformatter.SetKey(x_dsa);
// set the name of the hashing algorithm we used to create the hash code
x_deformatter.SetHashAlgorithm("SHA1");
// verify the DSA signature
bool x_signature_valid = x_deformatter.VerifySignature(x_hashcode, x_signature);

# Visual Basic .NET

' create the plaintext
Dim x_plaintext As Byte() = Encoding.Default.GetBytes("Programming .NET Security")

' create a hash code for the plaintext, using the SHA-1 algorithm
Dim x_hashcode As Byte() = HashAlgorithm.Create("SHA1").ComputeHash(x_plaintext)

' define the signature to verify

Dim x_signature As Byte() = New Byte() {&H7D, &H2B, &HD7, &H3D, &H88, &HCB, _
 &H1B, &H6B, &H4, &H62, &H95, &HBE, &H28, _
 &H59, &H3E, &HC5, &H40, &HDA, &H79, &HFE, _
 &H3B, &H25, &H8, &H4B, &H27, &HF1, &H31, _
 &H2A, &H6F, &H7C, &H6E, &H35, &H45, &H9A, _
 &H49, &H4C, &HA4, &H5E, &HE6, &HA0}

' create an instance of the DSA implementation class
Dim x_dsa As DSACryptoServiceProvider = New DSACryptoServiceProvider()

' create the signature deformatter
Dim x_deformatter As DSASignatureDeformatter = New
DSASignatureDeformatter()
' set the instance of the DSA algorithm that will verify the signature
x_deformatter.SetKey(x_dsa)
' set the name of the hashing algorithm we used to create the hash code
x_deformatter.SetHashAlgorithm("SHA1")

' verify the DSA signature
Dim x_signature_valid As Boolean = _
x_deformatter.VerifySignature(x_hashcode, x_signature)

编程XML 签名

.NET Framework 支持XML 签名说明书(通常被称为XMLDSIG),该说明书提供一套标准的方法来创建和代表XML 文件的签名。

本节介绍了. NET XMLDSIG 类。本节不会讨论所有的可用操作,也不会深入介绍标准本身。XMLDSIG标准是比较复杂的,本书不可能介绍所有的标准内容。有关XML签名说明书的完整 信息,详情请见Consortium的万维网网站:http://www.w3.org/TR/xmldsig-core。有关XMLDSIG 类的完整信息,详情请见.NET 文件。因该基本了解.NET Framework 是如何支持XML 的。

解释XMLDSIG

.NET Framework 包含了这样一种类,它能用来创建和验证XML 文件的XMLDSIG 签名;也就是说,.NET XMLDSIG 类允许签名XML 文件来创建签名,即XML 文件本身。

本节从头到尾都是以遵循简单的XML创建签名为基础来举例的,并假设该简单的XML 是一个名为book.xml 的文件:

<book>

<title>Programming .NET Security</title>
 <author>Adam Freeman</author>
 <author>Allen Jones</author>

</book>

System.Security.dll 包含了System.Security.Cryptography.Xml命名空间,而所有的.NET XMLDSIG 类都是包含在System.Security.Cryptography.Xml命名空间里的。

签名XML 文件

以下章节示范了如何为示例XML 文件创建一个XMLDSIG 签名。

创建引用

如果要创建一个签名,第一步应该创建一个对待签XML 文件的引用。.NET 引用类允许使用URL 字符串或数据流来创建一个引用。在创建了数字签名后,就可以从URL 或用于创建签名的数据流中读取XML数据了。以下语句示范了如何创建一个Reference对象:

# C#

// specify the URL of the document we want to sign
string x_url = "http://www.mydomain.com/book.xml";
// create a reference that points to the URL
Reference x_url_ref = new Reference(x_url);

// open a file stream for the document we want to sign
FileStream x_stream = new FileStream("book.xml", FileMode.Open);
// create a Reference that will use the stream
Reference x_stream_ref = new Reference(x_stream);

# Visual Basic .NET

' specify the URL of the document we want to sign
Dim x_url As String = "http:'www.mydomain.com/book.xml"
' create a reference that points to the URL
Dim x_url_ref As Reference = New Reference(x_url)

' open a file stream for the document we want to sign
Dim x_stream As FileStream = New FileStream("book.xml", FileMode.Open)
' create a Reference that will use the stream
Dim x_stream_ref As Reference = New Reference(x_stream)

创建SignedXML

System.Security.Cryptography.Xml.SignedXml类会负责创建签名文件。首先创建一个该类的实例,再使用用于创建签名的设置来配置该实例。使用默认的构造函数来创

建SignedXml 类的新实例,再使用AddReference 方法来添加之前章节创建的Reference对象:

# C#

SignedXml x_signed_xml = new SignedXml();

x_signed_xml.AddReference(x_stream_ref);

# Visual Basic .NET

Dim x_signed_xml As SignedXml = New SignedXml()

x_signed_xml.AddReference(x_stream_ref)

设置签名算法

需要创建一个新的非对称签名算法实例,并使用SigningKey 属性来将其分配给SignedXml对象,具体如以下语句所示:

# C#

// create a new instance of the DSA algorithm

DSA x_dsa = DSA.Create();

// configure the signing key

// ...

// set the algorithm for the SignedXml

x_signed_xml.SigningKey = x_dsa;

# Visual Basic .NET

' create a new instance of the DSA algorithm

Dim x_dsa As DSA = DSA.Create()

' configure the signing key

' ...

' set the algorithm for the SignedXml

x_signed_xml.SigningKey = x_dsa

这些语句会对用于创建签名的算法和密钥产生影响。.NET类支持使用RSA和DSA算法来生成XML 签名文件。

创建签名

通过调用ComputeSignature方法来创建签名。也可以通过调用GetXml方法来获得签名,并使用OuterXml属性来获得字符串的表示形式,具体如下所示:

# C#

x_signed_xml.ComputeSignature();
Console.WriteLine(x_signed_xml.GetXml().OuterXml);

# Visual Basic .NET

x_signed_xml.ComputeSignature()
Console.WriteLine(x_signed_xml.GetXml().OuterXml)

给我们的例子XML 创建的签名文件如下:

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
 <SignedInfo>
 <CanonicalizationMethod

 Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1" />
 <Reference>

           <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
 <DigestValue>eJgWGA+lXiqrOpXa+2NSBbO1HS4=</DigestValue>
 </Reference>
 </SignedInfo>
 <SignatureValue>u5zleIl7IY5MV8sxsirlGIqRaZ9LgExoKdQwgCMqaPxWZX7VOAcT9g==
 </SignatureValue>
</Signature>

要注意:XML 签名文件包含了签名算法(DSA)、散列算法(SHA-1)的信息,它也包含了基于64位加密的签名值。这是一个用于验证签名的信息的形式化表示形式,否 则就必须以ad hoc 方式在双方之间通信(例如,在交换签名数据之前,Alice 和Bob 会交换消息以协商散列和签名算法)。

在签名中嵌入对象

在前一节创建的签名是和与其相关的数据分隔开的,这和“编程数字签名”一节创建的其他签名是一样的。XMLDSIG 标准将源数据作为XML 签名文件的一部分包括进来。

如果要包括示例XML 文件,首先下载XML 文件,再使用它来创建一个DataObject 类的新实例:

# C#

// load the XML document
XmlDocument x_xml_doc = new XmlDocument();
x_xml_doc.Load("book.xml");

// create the data object for the xml document
DataObject x_obj = new DataObject();
x_obj.Data = x_xml_doc.ChildNodes;
x_obj.Id = "book";

# Visual Basic .NET

' load the XML document
Dim x_xml_doc As XmlDocument = New XmlDocument()
x_xml_doc.Load("book.xml")

' create the data object for the xml document
Dim x_obj As DataObject = New DataObject()
x_obj.Data = x_xml_doc.ChildNodes
x_obj.Uri = "book"

使用Data属性将XML 数据分配给DataObject,使用URI 属性将一个ID 分配给对象;例如,选择ID 为“book”。接下来创建一个新的引用,不是使用URL 或数据流来创建,而是使用一个“本地”引用来创建该新引用,其中会在对象ID 之前放置一个# 符号(在该例中,本地引用为“#book”):

# C#

// create the local reference
Reference x_local_reference = new Reference();
x_local_reference.Uri = "#book";

# Visual Studio .NET

' create the local reference
Dim x_local_reference As Reference = New Reference()
x_local_reference.Uri = "#book"

创建一个SignedXml类的新实例,使用AddReference和AddObjects方法来添加引用和数据:

# C#

// create the SignedXml instance
SignedXml x_signed_xml = new SignedXml();
// add the local reference
x_signed_xml.AddReference(x_local_reference);
// add the data object
x_signed_xml.AddObject(x_obj);

# Visual Basic .NET

' create the SignedXml instance
Dim x_signed_xml As SignedXml = New SignedXml()
' add the local reference
x_signed_xml.AddReference(x_local_reference)
' add the data object
x_signed_xml.AddObject(x_obj)

最后,设置签名算法的实例并计算签名:

# C#

// create a new instance of the DSA algorithm

DSA x_dsa = DSA.Create();

// configure the signing key
// ...

// set the algorithm for the SignedXml
x_signed_xml.SigningKey = x_dsa;

// compute the signature
x_signed_xml.ComputeSignature();
Console.WriteLine(x_signed_xml.GetXml().OuterXml);

# Visual Basic .NET

' create a new instance of the DSA algorithm
Dim x_dsa As DSA = DSA.Create()

' configure the signing key
' ...

' set the algorithm for the SignedXml
x_signed_xml.SigningKey = x_dsa

' compute the signature
x_signed_xml.ComputeSignature()
Console.WriteLine(x_signed_xml.GetXml().OuterXml)

签名结果如下所示;其包含的数据用粗体表示:

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
 <SignedInfo>
 <CanonicalizationMethod

 Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1" />
 <Reference URI="#book">

          <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />

 <DigestValue>1UhFInEywZYY/3eLgCqg5w+IROI=</DigestValue>

 </Reference>
 </SignedInfo>
 <SignatureValue>DUuD4ZJd8YiDLIr7HimDWGmCXYQDpX1jv1xRxKLgccw/lTyh3XjB6Q==
 </SignatureValue>

<Object Id="book">

 <book xmlns="">
 <title>Programming .NET Security</title>
 <author>Adam Freeman</author>
 <author>Allen Jones</author>

 </book>
 </Object>

</Signature>

通过包含数据这种方式就能创建一个XML 文件,该文件含有已签名的数据和如何创建签名的详细信息(散列计算和签名算法);它还包括签名本身,在交换已签消息时,签名允许Alice 将一个单XML 文件发送给Bob。

验证一个XML 签名

可以将XML 签名文件(将其保存为xmlsig.xml 文件)载入SignedXml类的一个实例,这样就能够实现验证签名:

# C#

// load the XML document
XmlDocument x_xml_doc = new XmlDocument();
x_xml_doc.Load("xmlsig.xml");

// create the SignedXml instance
SignedXml x_signed_xml = new SignedXml();

// get the node list from the XML sig doc
XmlNodeList x_node_list = x_xml_doc.GetElementsByTagName("Signature");
// load the XML signature document
x_signed_xml.LoadXml((XmlElement)x_node_list[0]);

# Visual Basic .NET

' load the XML document
Dim x_xml_doc As XmlDocument = New XmlDocument()
x_xml_doc.Load("xmlsig.xml")

' create the SignedXml instance
Dim x_signed_xml As SignedXml = New SignedXml()

' get the node list from the XML sig doc
Dim x_node_list As XmlNodeList = x_xml_doc.GetElementsByTagName("Signature")
' load the XML signature document
x_signed_xml.LoadXml(CType(x_node_list(0), XmlElement))

需要创建用于验证签名的非对称算法;还应该配置带有公共参数的算法类,这些公共参数是来源于生成签名的密钥对的。

一旦创建了算法,就可以为KeyInfo 类创建一个实例了,该类接收基于使用算法的DSAKeyValue或RSAKeyValue类的一个实例。通过设置KeyInfo属性的值来配置SignedXml类,从而使用非对称算法:

# C#

// create a new instance of the DSA algorithm
DSA x_dsa = DSA.Create();

// configure the signing key
// ...

// create the KeyInfo instance
KeyInfo x_info = new KeyInfo();

// add the DSA instance to the KeyInfo
x_info.AddClause(new DSAKeyValue(x_dsa));

// configure the SignedXml class to use the
// DSA keys for verification
x_signed_xml.KeyInfo = x_info;

# Visual Basic .NET

' create a new instance of the DSA algorithm
Dim x_dsa As DSA = DSA.Create()

' configure the signing key
' ...

' create the KeyInfo instance
Dim x_info As KeyInfo = New KeyInfo()

' add the DSA instance to the KeyInfo
x_info.AddClause(New DSAKeyValue(x_dsa))

' configure the SignedXml class to use the
' DSA keys for verification
x_signed_xml.KeyInfo = x_info

最后,通过调用SignedXml类的CheckSignature方法来验证签名;如果签名是有效的,该方法就返回“true”,否则返回“false”:

# C# // verify the signature bool x_signature_valid = x_signed_xml.CheckSignature(); # Visual Basic .NET ' verify the signature Dim x_signature_valid As Boolean = x_signed_xml.CheckSignature()

扩展.NET Framework

本书第15 章通过创建一个支持加密的ElGamal 算法实现来扩展.NET Framework。本节扩展了实现来支持创建和验证数字签名。

本书只提供ElGamal 算法的C# 实现。和几乎所有的加密算法一样,ElGamal 依赖于一种数学操作,如果不创建附加支持函数来弥补语言提供的受限数字支持,Visual Basic .NET 就不能实现这种数学操作。

解释ElGamal 签名函数

ElGamal算法使用同样的密钥来支持加密和数字签名。遵循密钥生成协议(第15章讨论过)创建的密钥能用于创建和验证数字签名,可以使用本节解释的函数来创建和验证。提醒大家注意:参数p、g 和y 是源于公钥,而参数x 则源于私钥。

创建一个ElGamal 签名的基本协议是:

验证一个ElGamal 签名,遵循以下协议:

要注意,和RSA 不一样,ElGamal 算法为数据加密和数字签名指定了不同的函数。

定义签名函数类

首 先要定义ElGamalSignature类,该类实现了基本签名和前一节描述的验证函数。using System; using System.Security.Cryptography; public class ElGamalSignature { 通过定义mod方法来确保:在使用BigInteger类包含的模块式算术支持时,总能接收到一个“正”的结果。

public static BigInteger mod(BigInteger p_base, BigInteger p_val) {
 BigInteger x_result = p_base % p_val;
 if (x_result < 0) {

 x_result += p_val;
 }
 return x_result;

}

CreateSignature类实现了ElGamal 签名函数,并创建了一个数字签名;这种方法的参数是用于签名的数据和密钥使用的数据:

public static byte[] CreateSignature(byte[] p_data,
ElGamalKeyStruct p_key_struct) {

首先生成一个随机数k,它是(p - 1)的相对素数。有关在BigInteger类中使用的方法总结,详情请见表15-6:

// define P -1
BigInteger x_pminusone = p_key_struct.P - 1;
// create K, which is the random number
BigInteger K;
do {

 K = new BigInteger();
 K.genRandomBits(p_key_struct.P.bitCount() -1, new Random());
} while (K.gcd(x_pminusone) != 1);

以下语句计算了签名元素的值:a和b。虽然这些语句看起来有些费解,但是它们确实能计算出所需要的值;BigInteger类要求这些语句看起来会比较复杂,而定义该类的mod方法这个事实也决定了它们看起来会比较复杂:

// compute the values A and B
BigInteger A = p_key_struct.G.modPow(K, p_key_struct.P);
BigInteger B = mod(mod(K.modInverse(x_pminusone)

 * (new BigInteger(p_data)
 -(p_key_struct.X*(A))),(x_pminusone)),(x_pminusone));

在完成计算之后,现在就可以创建一个字节数组来保存签名数据,并使用之前创建的签名元素来填充签名数据,从而创建完整的签名:

 // copy the bytes from A and B into the result array
 byte[] x_a_bytes = A.getBytes();
 byte[] x_b_bytes = B.getBytes();
 // define the result size
 int x_result_size = (((p_key_struct.P.bitCount() + 7) / 8) * 2);
 // create an array to contain the ciphertext
 byte[] x_result = new byte[x_result_size];
 // populate the arrays
 Array.Copy(x_a_bytes, 0, x_result, x_result_size / 2

- x_a_bytes.Length, x_a_bytes.Length);
Array.Copy(x_b_bytes, 0, x_result, x_result_size

 - x_b_bytes.Length, x_b_bytes.Length);

 // return the result array
 return x_result;
}

VerifySignature方法实现了ElGamal验证函数,该函数接收代表签名数据的参数、用于验证的签名和用于执行验证的密钥:

public static bool VerifySignature(byte[] p_data, byte[] p_sig,
 ElGamalKeyStruct p_key_struct) {

首先将签名分为两部分,创建BigInteger类的实例来代表签名元素a和b:

// define the result size
int x_result_size = p_sig.Length/2;

// extract the byte arrays that represent A and B
byte[] x_a_bytes = new byte[x_result_size];
Array.Copy(p_sig, 0, x_a_bytes, 0, x_a_bytes.Length);
byte[] x_b_bytes = new Byte[x_result_size];
Array.Copy(p_sig, x_result_size, x_b_bytes, 0, x_b_bytes.Length);

// create big integers from the byte arrays
BigInteger A = new BigInteger(x_a_bytes);
BigInteger B = new BigInteger(x_b_bytes);

以下剩余语句计算了两种验证元素,如果这两种元素具有相同值(签名是有效的),则返回2,否则(签名是无效的)则返回“false”。

 // create the two results
BigInteger x_result1 = mod(p_key_struct.Y.modPow(A, p_key_struct.P)

 * A.modPow(B, p_key_struct.P), p_key_struct.P);
        BigInteger x_result2 = p_key_struct.G.modPow(new BigInteger(p_data),

 p_key_struct.P);
 // return true if the two results are the same
 return x_result1 == x_result2;

 }
}

实现托管类方法

到现在为止,在托管算法类(本书第15章并没有完成它)中就定义了签名函数方法的实现。Sign和VerifySignature方法调用ElGamalSignature类的静态CreateSignature 和VerifySignature方法,在前一节定义过ElGamalSignature类。这两种方法都会检查:用户是否指定了密钥,用户是否会按要求创建一个新的密钥对:

public class ElGamalManaged : ElGamal {

// ... other methods

 public override byte[] Sign(byte[] p_hashcode) {

 if (NeedToGenerateKey()) {
 // we need to create a new key before we can export
 CreateKeyPair(KeySizeValue);

 }
 return ElGamalSignature.CreateSignature(p_hashcode, o_key_struct);
 }

public override bool VerifySignature(byte[] p_hashcode, byte[] p_signature) {

 if (NeedToGenerateKey()) {
 // we need to create a new key before we can export
 CreateKeyPair(KeySizeValue);

 }
 return ElGamalSignature.VerifySignature(p_hashcode,
 p_signature, o_key_struct);
 }

// ... other methods
}

该实现意味着:Sign和VerifySignature方法是直接以“未加工的”签名函数作为基础的,创建该签名函数时并没使用PKCS #1 格式化。以下章节定义了隔离的格式化类。

定义PKCS #1 助手类

通过定义ElGamalSignatureFormatHelper类来简化格式化程序和反格式化程序的实现。该类负责使用PKCS #1 格式化技术来格式化数据块:

using System;
using System.Security.Cryptography;

public class ElGamalSignatureFormatHelper {

首先定义表16-1 列出的算法ID 组。这些ID 是包含在PKCS #1 ——格式化数据里的;有关PKCS #1,详情请见“编程数字签名”一节。可以将算法ID 定义为字节数组来简化格式化过程:

private static byte[] MD5_BYTES

 = new byte[] {0x30, 0x20, 0x30, 0x0C, 0x06, 0x08, 0x2A, 0x86,
 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05, 0x05, 0x00,
 0x04, 0x10};

private static byte[] SHA1_BYTES
= new byte[] {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0E, 0x03,
 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14};

private static byte[] SHA256_BYTES
 = new byte[] {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,

 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
 0x00, 0x04, 0x20};

private static byte[] SHA384_BYTES

 = new byte[] {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05,
 0x00, 0x04, 0x30};

private static byte[] SHA512_BYTES

 = new byte[] {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86,
 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05,
 0x00, 0x04, 0x40};

GetHashAlgorithmID返回了一个代表散列算法ID 的字节数组,其中散列算法被指定为方法参数:

private static byte[] GetHashAlgorithmID(HashAlgorithm p_hash) {
 if (p_hash is MD5) {
 return MD5_BYTES;
 } else if (p_hash is SHA1) {
 return SHA1_BYTES;
 } else if (p_hash is SHA256) {
 return SHA256_BYTES;
 } else if (p_hash is SHA384) {
 return SHA384_BYTES;
 } else if (p_hash is SHA512) {
 return SHA512_BYTES;
 } else {
        throw new ArgumentException("Unknown hashing algorithm", "p_hash");
 }
}

CreateEMSA_PKCS1_v1_5_ENCODE使用PKCS #1 技术来格式化一个数据块,本章“编程数字签名”一节描述了PKCS #1 技术:

 public static byte[] CreateEMSA_PKCS1_v1_5_ENCODE(byte[] p_hashcode,
 HashAlgorithm p_hash_alg, int p_key_length) {

 // Concatenate the algorithm ID for the hash algorithm // and the hash code to form T byte[] x_algorithm_id = GetHashAlgorithmID(p_hash_alg); byte[] T = new byte[p_hashcode.Length + x_algorithm_id.Length]; Array.Copy(x_algorithm_id, 0, T, 0, x_algorithm_id.Length);        Array.Copy(p_hashcode, 0, T, x_algorithm_id.Length, p_hashcode.Length);

 // Generate an octet string PS consisting of p_key_length - T.Length
// 3 octets with hexadecimal value 0xff.
 // The length of PS will be at least 8 octets.
 int x_PS_length = p_key_length - T.Length -3;
 byte[] PS = new byte[x_PS_length < 0 ? 8 : x_PS_length];
 for (int i = 0; i < PS.Length; i++) {

PS[i] = 0xFF;
 }
// Concatenate PS, the DER encoding T, and other padding to form the
 // encoded message EM as EM = 0x00 || 0x01 || PS || 0x00 || T .
 byte[] EM = new byte[3 + PS.Length + T.Length];
 EM[0] = 0x00;
 EM[1] = 0x01;
 Array.Copy(PS, 0, EM, 2, PS.Length);
 EM[PS.Length + 2] = 0x00;
 Array.Copy(T, 0, EM, PS.Length + 3, T.Length);
 // Output EM.
 return EM;

 }
}

定义签名格式化程序类

ElGamalPKCS1SignatureFormatter类扩展了AsymmetricSignatureFormatter,并使用ElGamalSignatureFormatHelper来创建一个PKCS #1 ——格式化的散列码,将该代码传递给ElGamalManaged类的Sign方法:

using System;
using System.Security.Cryptography;

public class ElGamalPKCS1SignatureFormatter : AsymmetricSignatureFormatter { private string o_hash_name; // the hash algorithm to use private ElGamalManaged o_key; // the ElGamal algorithm

 public override void SetHashAlgorithm(string p_name) {
 o_hash_name = p_name;
 }

 public override void SetKey(AsymmetricAlgorithm p_key) {
 if (p_key is ElGamalManaged) {
 o_key = p_key as ElGamalManaged;
 } else {
 throw new ArgumentException(
 "Key is not an instance of ElGamalManaged", "p_key");
 }
 }

 public override byte[] CreateSignature(byte[] p_data) {
 if (o_hash_name == null || o_key == null) {
 throw new
                CryptographicException("Key and Hash Algorithm must be set");

 } else {
 // create the hashing algorithm
 HashAlgorithm x_hash_alg = HashAlgorithm.Create(o_hash_name);
 // create a PKCS1 formatted block from the data
 byte[] x_pkcs

 = ElGamalSignatureFormatHelper.CreateEMSA_PKCS1_v1_5_ENCODE(p_data,
 x_hash_alg, o_key.KeyStruct.P.bitCount());

 // create and return the signature
 return o_key.Sign(x_pkcs);
 }
 }
}

定义签名反格式化程序类

ElGamalPKCS1SignatureDeformatter类扩展了AsymmetricSignatureDeformatter,并使用ElGamalSignatureFormatHelper来创建一个PKCS #1——格式化的散列码,从而再使用ElGamalManaged类的VerifySignature方法来验证一个签名:

using System;
using System.Security.Cryptography;

public class ElGamalPKCS1SignatureDeformatter : AsymmetricSignatureDeformatter

 private string o_hash_name; // the hash algorithm to use private ElGamalManaged o_key; // the ElGamal algorithm

 public override void SetHashAlgorithm(string p_name) {
 o_hash_name = p_name;
 }

 public override void SetKey(AsymmetricAlgorithm p_key) {
 if (p_key is ElGamalManaged) {
 o_key = p_key as ElGamalManaged;
 } else {
 throw new ArgumentException(
 "Key is not an instance of ElGamalManaged", "p_key");
 }
 }

public override bool VerifySignature(byte[] p_data, byte[] p_signature) {
 if (o_hash_name == null || o_key == null) {
 throw new
                CryptographicException("Key and Hash Algorithm must be set");

 } else {
 // create the hashing algorithm
 HashAlgorithm x_hash_alg = HashAlgorithm.Create(o_hash_name);
 // create a PKCS1 formatted block from the data
 byte[] x_pkcs

               = ElGamalSignatureFormatHelper.CreateEMSA_PKCS1_v1_5_ENCODE(

 p_data, x_hash_alg, o_key.KeyStruct.P.bitCount());
 // create and return the signature
 return o_key.VerifySignature(x_pkcs, p_signature);

 }
 }
}

测试算法

以下语句测试了对未加工签名函数的支持和对PKCS #1 ——格式化ElGamal 签名的支持:

using System;
using System.Text;
using System.Security.Cryptography;

public class Test {

 public static void Main() {
 // define the byte array that we will use
 // as plaintext
 byte[] x_plaintext

 = Encoding.Default.GetBytes("Programming .NET Security");

 // Create an instance of the algorithm and generate some keys
 ElGamal x_alg = new ElGamalManaged();
 // set the key size - keep is small to speed up the tests
 x_alg.KeySize = 384;
 // extract and print the xml string (this will cause
 // a new key pair to be generated)
 string x_xml_string = x_alg.ToXmlString(true);
 Console.WriteLine("\n{0}\n", x_xml_string);

 // create a signature for the plaintext
 ElGamal x_sign_alg = new ElGamalManaged();
 // set the keys - note that we export with the
 // private parameters since we are signing data
 x_sign_alg.FromXmlString(x_alg.ToXmlString(true));
 byte[] x_signature = x_sign_alg.Sign(x_plaintext);

 // verify the signature
 ElGamal x_verify_alg = new ElGamalManaged();
 // set the keys - note that we export without the
 // private parameters since we are verifying data
 x_verify_alg.FromXmlString(x_alg.ToXmlString(false));
 Console.WriteLine("BASIC SIGNATURE: {0}",

 x_verify_alg.VerifySignature(x_plaintext, x_signature));

 // check the formatted signature support
 // - create a hash code for the plaintext
 HashAlgorithm x_hash_alg = HashAlgorithm.Create("SHA1");
 byte[] x_hashcode = x_hash_alg.ComputeHash(x_plaintext);

 // check the PKCS#1 signature formatting
 ElGamalPKCS1SignatureFormatter x_sig_formatter

 = new ElGamalPKCS1SignatureFormatter();
 x_sig_formatter.SetHashAlgorithm("SHA1");
 x_sig_formatter.SetKey(x_sign_alg);
 x_signature = x_sig_formatter.CreateSignature(x_hashcode);

 // verify the signature
 ElGamalPKCS1SignatureDeformatter x_sig_deformatter

 = new ElGamalPKCS1SignatureDeformatter();
 x_sig_deformatter.SetHashAlgorithm("SHA1");
 x_sig_deformatter.SetKey(x_verify_alg);

 Console.WriteLine("PKCS#1 SIGNATURE: {0}",
 x_sig_deformatter.VerifySignature(x_hashcode, x_signature));
 }

 private static bool CompareArrays(byte[] p_arr1, byte[] p_arr2) {
 for (int i = 0; i < p_arr1.Length; i++) {
 if (p_arr1[i] != p_arr2[i]) {
 return false;

 }
 }
 return true;

 }
}

posted on 2007-05-25 13:19  Xt Idt  阅读(2380)  评论(0)    收藏  举报