先说点题外话。首先要寒一下自己,有半年多没有更新blog,实在是偷懒至极。。其间心得倒也不少,如果只是贴代码,倒是挺容易,不过这样的post在我看来,是最垃圾的,肯定骂声无数;但要把问题讲清楚透彻,又比写代码要费劲得多
言归正传。在我看来,利用.NET开发应用程序,有两个比较突出的问题,就是性能和安全性。当然在Web应用上这两点倒不是很大的问题。
安全方面,除了强名称和混淆编译等部署端的保护外(其实没什么用),.NET还提供了代码访问权限,我们今天只讨论其中的强名称权限。
首先,我们知道,经过强名称签名的程序集,可以拥有严格的版本控制和命名保护。而通常情况下,同一个系统如果拥有多个程序集(DLL或EXE),一般都会使用相同的密钥去进行强名称签名。而第三方只能看到强名称的公钥部分。也就是说,除非拥有这个密钥,否则得不到相同的签名公钥。
强名称代码访问保护(StrongNameIdentityPermission),就是被保护的代码,只有相同签名的程序集才有权访问。这样,即使别人得到你的DLL,也无法使用它。
一、使用强名称代码保护
我们来看一个简单的例子。
1、创建强名称签名密钥对:
在命令行运行:sn -k KeyPair.snk,即可以将新产生的密钥对存放在KeyPair.snk文件中;
2、新建一个DLL项目(SNTest.DLL),在AssemblyInfo.cs中使用该强名称:
[assembly: AssemblyKeyFile("KeyPair.snk")]
3、创建测试方法:
public static string Hello() { return "Hello"; }
4、注意:这是关键。为这个方法添加强名称保护。
[System.Security.Permissions.StrongNameIdentityPermission(
System.Security.Permissions.SecurityAction.LinkDemand, PublicKey=
"0024000004800000940000000602000000240000"+
"5253413100040000010001005be5ddabc8109e4c"+
"0ffa55d067153c9871fc1991ca21401a24f2d8e4"+
"391a77671fc50fd9791e3d01506f33bac51882a5"+
"89a7ca17ac687fe2b00550776026f264bf838b7c"+
"aefe207597db9d4d52677d7e5342a4ec6760ca7a"+
"eff47a3578f1a924589b930c8292e324c5a451e5"+
"0134c41a01c234fb384a4a7c35656921eee45fb4") ]
public static string Hello() { return "Hello"; }
我们来分析一下。由于默认的代码访问权限是CLR控制的,所以直接通过在类或者方法前添加 StrongNameIdentityPermission 特性,就可以对代码进行保护。参数里的 PublicKey,就是允许访问该代码的程序集的强名称签名公钥部分。而SecurityAction则表示安全操作的类型。常用的有SecurityAction.Demand(要求堆栈里所有调用都拥有此权限)和SecurityAction.LinkDemand(要求堆栈里该方法的直接调用者拥有此权限)。
这里顺便提一下程序集强名称签名公钥的获得方法:在命令行输入 [sn -Tp 程序集名称]
sn -Tp SNTest.DLL
继续我们的例子。
5、再新建一个控制台EXE项目(SNTest.EXE),使用同一个 KeyPair.snk 进行签名
6、在主函数调用受保护的那个测试方法:
public static void Main() { Console.WriteLine( SNTest.Class1.Hello() ); }
运行看一下,现在是相同强名称程序集去访问,应该可以看到输出 "Hello"。
如果我们把 SNTest.EXE 的强名称去掉,或者对它使用另外一个密钥对进行签名,那么因为权限不够,将会抛出 SecurityException。
二、反射调用
大部分的情况,我们都可以用上面的方法进行代码的强名称访问权限保护。但是,你有没有和我一样,碰到过这样的应用:我们不是直接调用受保护方法,而是通过反射。这个时候仍会抛出 SecurityException!
原因应该比较好分析。强名称代码保护中,无论 SecurityAction.Demand 还是 SecurityAction.LinkDemand ,所要求的访问权限(强名称签名)都是很严格的。即,直接调用者,必须是指定签名的程序集。而使用反射,看起来好像也是我们的代码直接调用保护代码,但实际上反射是系统实现和调用的。流程如下:
我们的代码 -> 反射的类和方法(System.Refecltion下) -> 受保护代码
跟踪调用堆栈,得到的也是相同的结论。使用反射,必然会通过系统调用。
三、自己实现强名称代码保护
问题的解决,应该是有很多的思路和方法。比如继承 StrongNameIdentityPermission 类;或者继承 CodeAccessPermission,实现自己的代码保护权限。而我目前的解决方案,是自己写一个类(StrongNameCodeAccessPermission),检查它的调用堆栈里每个程序集的签名。之所以不用前个方案,一是因为.NET的代码访问实现起来比较复杂,二是自定义代码保护,还需要在机器上进行特殊的配置,反而不是很通用。
StrongNameCodeAccessPermission 是一个类,而不是Attribute,使用的时候也很简单,在需要保护的类的构造函数,或者特定方法的开头调用一下 StrongNameCodeAccessPermission .LinkDemand 即可:
public void AnotherProtected() {
StrongNameCodeAccessPermission .LinkDemand();
DoSomeThing();
}
以下是 StrongNameCodeAccessPermission 的代码,注释写得比较清楚,就不罗索了 ^^ 大家可以根据自己的需要修改这段代码。
一,使用 System.Diagnostics 里面的类获得调用堆栈;
二,为了解决前面谈到的反射问题,在检查时忽略了系统调用,也就是 System.DLL, System.Web.DLL, System.Windows.Forms.DLL 这类微软的程序集。它们的强名称公钥是:b77a5c561934e089 和 b03f5f7f11d50a3a,如果有遗漏的话大家补充上即是。
三,我这里获得强名称签名公钥,是通过 Assembly.GetCallingAssembly().GetName().GetPublicKeyToken() , 其实也可以从证据里面获得。网上有此类资料。
四,目前检查的是调用方程序集必须和被保护代码的程序集拥有相同的强名称签名。这应该是很常见的场合。。
BTW:千万不要相信这能保护你的代码和程序...
// @FileName : StrongNameCodeAccessPermission.cs
// @Project : Common
// @CopyRight : Hooyee Network & Software co.,ltd.
// @Author : Kriss
// @Description : 强名称代码访问权限
// @LastUpdate : Kriss, 2005-09-02 10:30
// ==============================================================================
using System;
using System.Reflection;
using System.Diagnostics;
namespace Hooyee.Library.Utils {
/// <summary>
/// 对 System.Security.Permissions.StrongNameIdentityPermission 的简化与修正。
/// 只允许与待检查程序集相同强名称签名的程序才可以访问代码。
/// 系统 StrongNameIdentityPermission 权限对于相同强名称程序集的反射调用会抛出异常。本类不会。
/// </summary>
public sealed class StrongNameCodeAccessPermission {
private const string EXCEPTION_MESSAGE = "No Permission To Access The Code!";
private static readonly string[] SYSTEM_PUBLIC_KEYS; // 系统动态库的强名称公钥
private StrongNameCodeAccessPermission() {
}
static StrongNameCodeAccessPermission () {
SYSTEM_PUBLIC_KEYS = new string[2];
SYSTEM_PUBLIC_KEYS[0] = "b77a5c561934e089";
SYSTEM_PUBLIC_KEYS[1] = "b03f5f7f11d50a3a";
}
// 将字节转换成16进制字符
private static string ToHex( byte[] data ) {
string result = string.Empty;
for ( int i = 0; i < data.Length; i++ ) {
result += Convert.ToString( data[i], 16 ).ToLower().PadLeft( 2, '0' );
}
return result;
}
// 检查是否是系统调用
private static bool IsSystemCalling( string publicKeyToken ) {
for (int j=0; j < SYSTEM_PUBLIC_KEYS.Length; j++ ) {
if ( publicKeyToken == SYSTEM_PUBLIC_KEYS[j] ) {
return true;
}
}
return false;
}
/// <summary>
/// 对堆栈中所有的调用进行强名称代码访问检查。
/// </summary>
/// <remarks>本方法必须被需要进行强名称检查的代码段<B>直接调用</B>。</remarks>
public static void Demand() {
// 获得验证代码段所在的程序集的强名称公钥标识
string callingAssemblyKey = ToHex(Assembly.GetCallingAssembly().GetName().GetPublicKeyToken());
// 获得访问堆栈,跳过本方法以及需验证代码的直接调用堆栈
StackTrace st = new StackTrace( 2, false );
// 遍历检查所有上级调用堆栈
string stackAssemblyKey;
for ( int i=2; i < st.FrameCount; i++ ) {
// 获得调用方程序集的强名称公钥标识
stackAssemblyKey = ToHex(st.GetFrame( i ).GetMethod().DeclaringType
.Assembly.GetName().GetPublicKeyToken());
// 如果是相同强名称签名的程序集,允许调用
if ( callingAssemblyKey == stackAssemblyKey )
continue;
// 允许系统动态库的访问权限
if ( IsSystemCalling( stackAssemblyKey ) == false ) {
throw new System.Security.SecurityException( EXCEPTION_MESSAGE );
}
}
}
/// <summary>
/// 对堆栈中的非系统直接调用者进行强名称代码访问检查。
/// </summary>
/// <remarks>
/// 1. 本方法必须被需要进行强名称检查的代码段<B>直接调用</B>。
/// 2. 本方法只检查第一级直接调用者(如果上级调用是系统函数,则继续向上递归,主要考虑到反射的问题)。
/// 3. 系统的 System.Security.Permissions.StrongNameIdentityPermission 可以检查直接调用者,但如果是通过反射调用,则会报错。
/// 4. 除非对安全要求极高,要求检查堆栈中的所有调用者,一般情况下极力推荐使用本方法,只检查直接调用方的权限即可。
/// </remarks>
public static void LinkDemand() {
// 获得验证代码段所在的程序集的强名称公钥标识
string callingAssemblyKey = ToHex(Assembly.GetCallingAssembly().GetName().GetPublicKeyToken());
// 获得访问堆栈,跳过本方法以及需验证代码的直接调用堆栈
StackTrace st = new StackTrace( 2, false );
// 遍历检查所有上级调用堆栈
string stackAssemblyKey;
for ( int i=2; i < st.FrameCount; i++ ) {
// 获得调用方程序集的强名称公钥标识
stackAssemblyKey = ToHex(st.GetFrame( i ).GetMethod().DeclaringType
.Assembly.GetName().GetPublicKeyToken());
// 如果上级调用是系统函数,继续向上递归
if ( IsSystemCalling( stackAssemblyKey ) )
continue;
// 判断非系统直接调用者的强名称签名
if ( callingAssemblyKey == stackAssemblyKey ) {
return;
} else {
throw new System.Security.SecurityException( EXCEPTION_MESSAGE + stackAssemblyKey );
}
}
}
}
}