安全性
为了确保应用程序的安全,安全性有几个重要方面需要考虑。一是应用程序的用户,访问应用程序的是一个真正的用户,还是伪装成用户的某个人?如何确定这个用户是可以信任的?如本章所述,确保应用程序安全的用户方面是一个两阶段过程:用户首先需要进行身份验证,再进行授权,以验证该用户是否可以使用需要的资源。
对于在网络上存储或发送的数据呢?例如,有人可以通过网络嗅探器访问这些数据吗?这里,数据的加密很重要。一些技术,Windows Communication Foundation(WCF)通过简单的配置提供了加密功能,所以可以看到后台执行了什么操作。
另一方面是应用程序本身。如果应用程序驻留在Web提供程序上,如何禁止应用程序执行对服务器有害的操作?
木章将讨论.NET中有助于管理安全性的一些特性,其中包括.NET怎样避开恶意代码、怎样管理安全性策略,以及怎样通过编程访问安全子糸统等。
验证用户信息
安全性的两个基本支柱是身份验证和授权。身份验证是标识用户的过程,授权在验证了所标识用户是否可以访问特定资源之后进行。本节介绍如何使用标识符和principals获得用户的信息。
使用Windows标识
使用标识可以验证运行应用程序的用户。Windowslndentity类表示一个Windows用户。如果没有用Windows账户标识用户,也可以使用实现了Ildentity接口的其他类。通过这个接口可以访问用户名、该用户是否通过身份验证,以及验证类型等信息。
principal是一个包含用户的标识和用户的所属角色的对象。IPrincipal接口定义了Identity属性和IslnRole()方法,Identity属性返回Ildentity对象;在IslnRole()方法中,可以验证用户是否是指定角色的一个成员。角色是有相同安全权限的用户集合,同时它是用户的管理单元。角色可以是Windows组或自己定义的一个字符串集合
.NET中的Principal类有WindowsPrincipal、GenericPrincipal和RolePrincipal。从.NET4.5开始,这些Principal类型派生于基类ClaimsPrincipal。还可以创建实现了IPrincipal接口或派生于ClaimsPrincipaI的自定义Principal类。
/// <summary>
/// 调用方法ShowIdentityInformation把WindowsIdentity的信息写到控制台,
/// 调用ShowPrincipal写入可用于principals的额外信息,
/// 调用ShowClaims写入声明信息
/// </summary>
public static void WindowsPrincipalStart() {
WindowsIdentity identity = ShowIdentityInformation();
WindowsPrincipal principal = ShowPrincipal(identity);
ShowClaims(principal.Claims);
}
/// <summary>
/// ShowIdentityInformation方法通过调用WindowsIdentity的静态方法GetCurrent,
/// 创建一个WindowsIdentity对象,并访问其属性,来显示身份类型、登录名、
/// 是否进行了身份验证、身份验证类型、匿名用户和AccessToken等
/// </summary>
/// <returns></returns>
private static WindowsIdentity ShowIdentityInformation()
{
WindowsIdentity identity = WindowsIdentity.GetCurrent();
if (identity == null)
{
Console.WriteLine("没有windows 用户");
return null;
}
Console.WriteLine($"身份类型:{identity}");
Console.WriteLine($"登录名:{identity.Name}");
Console.WriteLine($"是否进行了身份验证:{identity.IsAuthenticated}");
Console.WriteLine($"身份验证类型:{identity.AuthenticationType}");
Console.WriteLine($"是否为匿名用户{identity.IsAnonymous}");
Console.WriteLine($"AccessToken:{identity.AccessToken.DangerousGetHandle()}");
Console.WriteLine();
return identity;
}
所有的标识类,例如Windowsldentity,都实现了Ildentity接口,该接口包含3个属性(AuthenticationType、IsAuthenticated和Name),便于所有的派生标识类实现它们。Windowsldentity的其他属性都专用于这种标识。输出结果如下
身份类型:System.Security.Principal.WindowsIdentity
登录名:DESKTOP-V5RCBJA\Stone
是否进行了身份验证:True
身份验证类型:NTLM
是否为匿名用户False
AccessToken:700
Windows Principal
principal包含一个标识,提供额外的信息,比如用户所属的角色。principal实现了IPnncipal接口,提供了方法IslnRole和Identity属性。在Windows中,用户所属的所有Windows组映射到角色。重载IslnRole方法,以接受安全标识符、角色字符串或WindowsBuiltInRole枚举的值。
/// <summary>
/// 验证用户是否属于内置的角色User和Administrator
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
private static WindowsPrincipal ShowPrincipal(WindowsIdentity identity)
{
Console.WriteLine("显示组成员信息。");
WindowsPrincipal principal = new WindowsPrincipal(identity);
if (principal == null)
{
Console.WriteLine("无组成员");
return null;
}
Console.WriteLine($"Users?{principal.IsInRole(WindowsBuiltInRole.User)} ");
Console.WriteLine($"Administrator?{principal.IsInRole(WindowsBuiltInRole.Administrator)}");
Console.WriteLine();
return principal;
}
结果
显示组成员信息。
Users?True
Administrator?False
很明显,如果能很容易地访问当前用户及其角色的详细信息,然后使用那些信息决定允许或拒绝用户执行某些动作,这就非常有好处。利用角色和Windows用户组,管理员可以完成使用标准用户管理工具所能完成的工作,这样,在用户的角色改变时,通常可以避免更改代码。
自.NET4.5以来,所有principal类都派生自基类ClaimsPrincipal。这样,可以使用principal对象的Claims属性来访问用户的声明。
使用声明
声明(claim)提供了比角色更大的灵活性。声称是一个关于标识(来自权威机构)的语句。权威机构如Active Directory或Microsoft Live账户身份验证服务,建立关于用户的声称,例如,用户名的声明、用户所属的组的声明或关于年龄的声称。用户己经21岁了,有资格访问特定的资源吗?
/// <summary>
/// 验证用户是否属于内置的角色User和Administrator
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
private static WindowsPrincipal ShowPrincipal(WindowsIdentity identity)
{
Console.WriteLine("显示组成员信息。");
WindowsPrincipal principal = new WindowsPrincipal(identity);
if (principal == null)
{
Console.WriteLine("无组成员");
return null;
}
Console.WriteLine($"Users?{principal.IsInRole(WindowsBuiltInRole.User)} ");
Console.WriteLine($"Administrator?{principal.IsInRole(WindowsBuiltInRole.Administrator)}");
Console.WriteLine();
return principal;
}
/// <summary>
/// 访问一组声明,把主题、发行人、声明类型和更多选项写到控制台
/// </summary>
/// <param name="claims"></param>
private static void ShowClaims(IEnumerable<Claim> claims)
{
Console.WriteLine("声明");
foreach (var claim in claims)
{
Console.WriteLine($"主题:{claim.Subject}");
Console.WriteLine($"颁发者:{claim.Issuer}");
Console.WriteLine($"声明类型:{claim.Type}");
Console.WriteLine($"值类型:{claim.ValueType}");
Console.WriteLine($"值:{claim.Value}");
foreach (var prop in claim.Properties)
{
Console.WriteLine($"\tProperty:{prop.Key}{prop.Value}");
}
Console.WriteLine();
}
}
结果
声明
主题:System.Security.Principal.WindowsIdentity
颁发者:AD AUTHORITY
声明类型:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
值类型:http://www.w3.org/2001/XMLSchema#string
值:DESKTOP-V5RCBJA\Stone
主题:System.Security.Principal.WindowsIdentity
颁发者:AD AUTHORITY
声明类型:http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid
值类型:http://www.w3.org/2001/XMLSchema#string
值:S-1-5-21-806069017-2007770079-1067169437-1001
Property:http://schemas.microsoft.com/ws/2008/06/identity/claims/windowssubauthorityNTAuthority
......
可以从声明的提供程序中把声明添加到Windows标识。还可以从简单的客户端程序中添加声明,如年龄声称:
identity.AddClaim(new Claim("年龄","25"));
使用程序中的声明,相信这个声明。这个声明是真的一一一是25岁吗?声称也可以都是谎言。从客户机应用程序中添这个声明,可以看到,声明的发行人是LOCAL AUTHORITY。AD AUTHORITY(Active Directory)的信息更值得信赖,但这里需要信任Active Directory系统管理员。
Windowsldentity派生自基类Claimsldentity,提供了几个方法来检查声明,或检索特定的声明。为了测试声明是古可用,就可以使用HasClaim方法.
bool hasName = identity.HasClaim(c=>c.Type==ClaimTypes.Name);
要检索特定的声称,FindAll方法需要一个谓词来定义匹配:
var groupClaims = identity.FindAll(c=>c.Type==ClaimTypes.Groupsid);
注意:声明类型可以是一个简单的字符串,例如前面使用的”Age”类型.ClaimType定义了一组已知的类型,例如Country、Email、Name、MobilePhone、UserData、Surname、PostalCode等
注意:ASP.NET Web应用程序用户的身份验证参见41
加密数据
机密数据应得到保护,从而使未授权的用户不能读取它们。这对于在网络中发送的数据或存储的数据都有效。可以用对称或不对称密钥来加密这些数据。
通过对称密钥,可以使用同一个密钥进行加密和解密。与不对称的加密相比,加密和解密使用不同的密钥:公钥/私钥。如果使用一个公钥进行加密,就应使用对应的私钥进行解密,而不是使用公钥解密。同样,如果使用一个私钥加密,就应使用对应的公钥解密,而不是使用私钥解密。不可能从私钥中计算出公钥,也不可能从公钥中计算出私钥。
公钥/私钥总是成对创建。公钥可以由任何人使用,它甚至可以放在web站点上,但私钥必须安全地加锁。为了说明加密过程,下面看看使用公钥和私钥的例子。
使用对称密钥的加密和解密算法比使用非对称密钥的算法快得多。对称密钥的问题是密钥必须以安全的方式互换。在网络通信中,一种方式是先使用非对称的密钥进行密钥互换,再使用对称密钥加密通过网络发送的数据。
在.NET Framework中,可以使用System.Security.Cryptography名称空间中的类来加密。它实现了几个对称算法和非对称算法。有几个不同的算法类用于不同的目的。一些类以Cng作为前缀或后缀。CNG是CryptographyNextGeneration的简称,是本机CryptoAPI的更新版本,这个API可以使用基于提供程序的模型,编写独立于算法的程序。
表列出了System.Security.Cryptography名称空间中的加密类及其功能。没有Cng、Managed或CryptoServiceProvider后缀的类是抽象基类,如MDS。Managed后缀表示这个算法用托管代码实现,其他类可能封装了本地WindowsAPI调用。CryptoServiceProvider后缀用于实现了抽象基类的类,Cng后缀用于利用新Cryptography CNG API的类。
创建和验证签名
如何使用ECDSA算法进行签名。Alice创建了一个签名,它用Alice的私钥加密,可以使用Alice的公钥访问。因此保证该签名来自于Alice。
private static CngKey _aliceKeySignature;
private static byte[] _alicePublicKeyBlob;
/// <summary>
/// 创建Alice的密钥,给字符串"Alice"签名,最后使用公钥验证该签名是否真的来自于Alice。
/// </summary>
public static void SigningStart()
{
InitAliceKeys();
byte[] aliceData = Encoding.UTF8.GetBytes("Alice");
byte[] aliceSignature = CreateSignature(aliceData, _aliceKeySignature);
Console.WriteLine($"Alice创建签名:{Convert.ToBase64String(aliceSignature)}");
if (VerifySignature(aliceData, aliceSignature, _alicePublicKeyBlob))
{
Console.WriteLine("Alice签名已成功验证");
}
}
/// <summary>
/// 为Alice创建新的密钥对。因为这个密钥对存储在一个静态字段中,所以可以从其他方法中访问它。
/// 除了使用CngKey类创建密钥对之外,还可以打开存储在密钥存储器中的己有密钥。
/// 通常Alice在其私有存储器中有一个证书,其中包含了一个密钥对,该存储器可以用CngKey.Open()方法访问。
/// </summary>
private static void InitAliceKeys()
{
// CngKey类的Creat()方法把该算法作为一个参数,为算法定义密钥对。
_aliceKeySignature = CngKey.Create(CngAlgorithm.ECDsaP521);
// 通过Export()方法,导出密钥对中的公钥。这个公钥可以提供给Bob,来验证签名。Alice保留其私钥。
_alicePublicKeyBlob = _aliceKeySignature.Export(CngKeyBlobFormat.GenericPublicBlob);
}
/// <summary>
/// 有了密钥对,Alice就可以使用ECDsaCng类创建签名了。
/// 这个类的构造函数从Alice那里接收包含公钥和私钥的CngKey类。
/// 再使用私钥,通过SignData0方法给数据签名。SignData()方法在.NET core中略有不同。.NETcore需要如下算法
/// </summary>
/// <param name="aliceData"></param>
/// <param name="_aliceKeySignature"></param>
/// <returns></returns>
private static byte[] CreateSignature(byte[] aliceData, CngKey _aliceKeySignature)
{
byte[] signature;
using (var signingAlg = new ECDsaCng(_aliceKeySignature))
{
#if NET46
signature = signingAlg.SignData(aliceData);
signingAlg.Clear();
#else
signature = signingAlg.SignData(aliceData, HashAlgorithmName.SHA512);
#endif
}
return signature;
}
/// <summary>
/// 要验证签名是否真的来自于Alice,Bob使用Alice的公钥检查签名。包含公钥blob的字节数组可以用静态方法Import()导入CngKey对象。
/// 然后使用ECDsaCng类,调用VerifyData()方法来验证签名。
/// </summary>
/// <param name="aliceData"></param>
/// <param name="aliceSignature"></param>
/// <param name="_alicePublicKeyBob"></param>
/// <returns></returns>
private static bool VerifySignature(byte[] data, byte[] signature, byte[] pubKey)
{
bool retValue = false;
using (CngKey key = CngKey.Import(pubKey, CngKeyBlobFormat.GenericPublicBlob))
using (var signingAlg = new ECDsaCng(key))
{
#if NET46
retValue = signingAlg.VerifyData(data, signature);
signingAlg.Clear();
#else
retValue = signingAlg.VerifyData(data, signature, HashAlgorithmName.SHA512);
#endif
}
return retValue;
}
}
结果
Alice创建签名:ALCyvRdlH9tqUlOMJNvWF2ft7OcVzqyPxNxEI5hRivK1qkIvMNQe44vV2j3jAfT/nqFhh8Nc1FqGk0L7Cn4GoZDxAOUKzW+QbRdLVVd3yDW3QUFjspqgtsLeqeQczNKLGMxdwBYbaK910ia0oCfxSE40GmTxn7b+W9YJQStz+TNv6ivK
Alice签名已成功验证
实现安全的数据交换
它使用ECDiffe-HeIIman算法交换一个对称密钥,以进行安全的传输。
Alice创建了一条加密的消息,并把它发送给Bobo在此之前,要先为Alice和Bob创建密钥对。Bob只能访问Alice的公钥,Alice也只能访问Bob的公钥。
private static CngKey _aliceKey;
private static CngKey _bobKey;
private static byte[] _alicePubKeyBlob;
private static byte[] _bobPubKeyBlob;
public static void SecureTransferStart()
{
RunAsync().Wait();
}
private static async Task RunAsync()
{
try
{
CreateKeys();
byte[] encryptedData = await AliceSendsDataAsync("一条发给bob加密消息");
await BobReceivesDataAsync(encryptedData);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
/// <summary>
/// 使用EC DiffieHellman 521算法创建密钥
/// </summary>
private static void CreateKeys()
{
_aliceKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP521);
_bobKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP521);
_alicePubKeyBlob = _aliceKey.Export(CngKeyBlobFormat.EccPublicBlob);
_bobPubKeyBlob = _aliceKey.Export(CngKeyBlobFormat.EccPublicBlob);
}
/// <summary>
/// 在AliceSendsDataAsync()方法中,包含文本字符的字符串使用Encoding类转换为一个字节数组。
/// 创建一个ECDiffieHellmanCng对象,用Alice的密钥对初始化它。
/// Alice调用DeriveKeyMaterial()方法,从而使用其密钥对和Bob的公钥创建一个对称密钥。
/// 返回的对称密钥使用对称算法AES加密数据。
/// AesCryptoServiceProvider需要密钥和一个初始化矢量(IV)。
/// IV从GenerateIV()方法中动态生成,对称密钥用EC Diffe-He11man算法交换,但还必须交换IV。
/// 从安全性角度来看,在网络上传输未加密的IV是可行的一一只是密钥交换必须是安全的。
/// IV存储为内存流中的第一项内容,其后是加密的数据,其中,CryptoStream类使用AesCryptoServiceProvider类创建的encryptor。
/// 在访问内存流中的加密数据之前,必须关闭加密流。否则,加密数据就会丢失最后的位。
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
private static async Task<byte[]> AliceSendsDataAsync(string message)
{
Console.WriteLine($"Alice发送了信息{message}");
byte[] rawData = Encoding.UTF8.GetBytes(message);
byte[] encryptedData = null;
using (var aliceAlgorithm = new ECDiffieHellmanCng(_aliceKey))
using (CngKey bobPubkey = CngKey.Import(_bobPubKeyBlob, CngKeyBlobFormat.EccPublicBlob))
{
byte[] symmKey = aliceAlgorithm.DeriveKeyMaterial(bobPubkey);
Console.WriteLine($"Alice创建对称密钥——bob的公共密钥信息{Convert.ToBase64String(symmKey)}");
using (var aes = new AesCryptoServiceProvider())
{
aes.Key = symmKey;
aes.GenerateIV();
using (ICryptoTransform encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
await ms.WriteAsync(aes.IV, 0, aes.IV.Length);
cs.Write(rawData, 0, rawData.Length);
}
encryptedData = ms.ToArray();
}
aes.Clear();
}
}
Console.WriteLine($"Alice:加密消息{ Convert.ToBase64String(encryptedData)}");
Console.WriteLine();
return encryptedData;
}
/// <summary>
/// Bob从BobReceivesDataAsync()方法的参数中接收加密数据。
/// 首先,必须读取未加密的初始化矢量。AesCryptoServiceProvider类的BlockSize属性返回块的位数。
/// 位数除以8,就可以计算出字节数。最快的方式是把数据右移3位。
/// 右移1位就是除以2,右移2位就是除以4,右移3位就是除以8。
/// 在for循环中,包含未加密IV的原字节的前几个字节写入数组iv中。
/// 接着用Bob的密钥对实例化一个ECDiffieHellmanCng对象。
/// 使用Alice的公钥,从DeriveKeyMaterial0方法中返回对称密钥。
/// 比较Alice和Bob创建的对称密钥,可以看出所创建的密钥值相同。
/// 使用这个对称密钥和初始化矢量,来自Alice的消息就可以用AesCryptoServiceProvider类解密。
/// </summary>
/// <param name="encryptedData"></param>
/// <returns></returns>
private static async Task BobReceivesDataAsync(byte[] encryptedData)
{
Console.WriteLine($"Bob已接收加密数据");
byte[] rawData = null;
var aes = new AesCryptoServiceProvider();
int nBytes = aes.BlockSize >> 3;
byte[] iv = new byte[nBytes];
for (int i = 0; i < iv.Length; i++)
{
iv[i] = encryptedData[i];
}
using (var bobAlgorithm = new ECDiffieHellmanCng(_bobKey))
using (CngKey alicePubKey = CngKey.Import(_alicePubKeyBlob, CngKeyBlobFormat.EccPublicBlob))
{
byte[] symmKey = bobAlgorithm.DeriveKeyMaterial(alicePubKey);
Console.WriteLine($"Bob创建对称密钥——Alice的公共密钥信息{Convert.ToBase64String(symmKey)}");
aes.Key = symmKey;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor())
using (MemoryStream ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
{
await cs.WriteAsync(encryptedData, nBytes, encryptedData.Length - nBytes);
}
rawData = ms.ToArray();
Console.WriteLine($"Bob解密信息:{Encoding.UTF8.GetString(rawData)}");
}
aes.Clear();
}
}
结果
这里写代码片
使用RSA签名和散列
RSA是一个广泛使用的非对称算法。
本例为,Alice创建一个文档,散列它,以确保它不会改变,给它加上签
名,保证是Alice生成了文档。Bpb接收文件,并检查Alice的担保,以确保文件没有被篡改。
开始Alice的任务,调用方法AliceTasks,来创建一个文档、散列码和签名。然后把这些信息传递给Bob的任务,调用方法BobTasks
private static CngKey _aliceKey;
private static byte[] _alicePubKeyBlob;
public static void RSAStart()
{
byte[] document;
byte[] hash;
byte[] signature;
AlickTask(out document, out hash, out signature);
BobTasks(document, hash, signature);
}
/// <summary>
/// 首先创建Alice所需的密钥,将消息转换为一个字节数组,散列字节数组,并添加一个签名
/// </summary>
/// <param name="data"></param>
/// <param name="hash"></param>
/// <param name="signature"></param>
private static void AlickTask(out byte[] data, out byte[] hash, out byte[] signature)
{
InitAliceKeys();
data = Encoding.UTF8.GetBytes("致Alice");
hash = HashDocument(data);
signature = AddSignatureToHash(hash,_aliceKey);
}
/// <summary>
/// Alice所需的密钥是使用CngKey类创建的。现在正在使用RSA算法,
/// 把CngAlgorithm.Rsa枚举值传递到Create方法,来创建公钥和私钥。
/// 公钥只提供给Bob,所以公钥用Export方法提取
/// </summary>
private static void InitAliceKeys()
{
_aliceKey = CngKey.Create(CngAlgorithm.Rsa);
_alicePubKeyBlob = _aliceKey.Export(CngKeyBlobFormat.GenericPublicBlob);
}
/// <summary>
/// 为文档创建一个散列码。散列码使用一个散列算法SHA384类创建。
/// 不管文档存在多久,散列码的长度总是相同。
/// 再次为相同的文档创建散列码,会得到相同的散列码。
/// Bob需要在文档上使用相同的算法。
/// 如果返回相同的散列码,就说明文档没有改变。
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private static byte[] HashDocument(byte[] data)
{
using (var hashAlg = SHA384.Create())
{
return hashAlg.ComputeHash(data);
}
}
/// <summary>
/// 添加签名,可以保证文档来自Alice。在这里,使用RSACng类给散列签名。
/// Alice的CngKey(包括公钥和私钥)传递给RSACng类的构造函数;
/// 签名通过调用SignHash方法创建。给散列签名时,SignHash方法需要了解散列算法;
/// HashAlgorithmName.SHA384是创建散列所使用的算法。
/// 此外,需要RSA填充。RSASignaturePadding枚举的可选项是PSS和Pkcsl:
/// </summary>
/// <param name="hash"></param>
/// <param name="aliceKey"></param>
/// <returns></returns>
private static byte[] AddSignatureToHash(byte[] hash, CngKey key)
{
using (var signAlg = new RSACng(key))
{
byte[] signed = signAlg.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pss);
return signed;
}
}
/// <summary>
/// Alice散列并签名后,Bob的任务可以在BobTasks方法中开始。Bob接收文档数据、散列码和签名,他使用Alice的公钥。首先,Alice的公钥使用CngKey.Import导入,分配给aliceKey变量。接下来,Bob使用辅助方法IsSignatureValid和IsDocumentUnchanged,来验证签名是否有效,文档是否不变。只有在两个条件是true时,文档写入控制台:
/// </summary>
/// <param name="document"></param>
/// <param name="hash"></param>
/// <param name="signature"></param>
private static void BobTasks(byte[] data, byte[] hash, byte[] signature)
{
CngKey aliceKey = CngKey.Import(_alicePubKeyBlob, CngKeyBlobFormat.GenericPublicBlob);
if (!IsSignatureValid(hash,signature,aliceKey))
{
Console.WriteLine("签名不合规");
return;
}
if (!IsDocumentUnChanged(hash, data))
{
Console.WriteLine("文档已经变化");
return;
}
Console.WriteLine("签名合规,文档无变化");
Console.WriteLine($"从Alice获得的文档:{Encoding.UTF8.GetString(data,0,data.Length)}");
}
/// <summary>
/// 为了验证签名是否有效,使用Alice的公钥创建RSACng类的一个实例。
/// 通过这个类,使用VerifyHash方法传递散列、签名、早些时候使用的算法信息。
/// 现在Bob知道,信息来自Alice:
/// </summary>
/// <param name="hash"></param>
/// <param name="signature"></param>
/// <param name="key"></param>
/// <returns></returns>
private static bool IsSignatureValid(byte[] hash, byte[] signature, CngKey key)
{
using (var sianAlg = new RSACng(key))
{
return sianAlg.VerifyHash(hash, signature, HashAlgorithmName.SHA384, RSASignaturePadding.Pss);
}
}
/// <summary>
/// 为了验证文档数据没有改变,Bob再次散列文件,并使用LINQ扩展方法SequenceEqual,
/// 验证散列码是否与早些时候发送的相同。如果散列值是相同的,就可以假定文档没有改变
/// </summary>
/// <param name="hash"></param>
/// <param name="data"></param>
/// <returns></returns>
private static bool IsDocumentUnChanged(byte[] hash, byte[] data)
{
byte[] newHash = HashDocument(data);
return newHash.SequenceEqual(hash);
}
结果
签名合规,文档无变化
从Alice获得的文档:致Alice
实现数据的保护(.NET Core)
与加密相关的另一个NET特性是新的.NET核心库,它支持数据保护。名称空间System.Secunty.DataProtection包含DpApiDataProtector类,而这个类包装了本机Windows Data Protection API(DPAPI)。这些类并不提供web服务器上需要的灵活性和功能一一一所以ASP.NET团队创建了Microsoft.AspNet.DataProtection名称空间中的类。
使用这个库的原因是为日后的检索存储可信的信息,但存储媒体(如使用第三方的托管环境)不能信任自己,所以信息需要加密存储在主机上。
示例应用程序是一个简单的ConsoleApplication(Package),允许使用数据保护功能读写信息。
private const string readOption = "-r";
private const string writeOption = "-w";
private static readonly string[] options = { readOption, writeOption };
/// <summary>
/// 使用-r和-w命令行参数,可以启动控制台应用程序,读写存储器。
/// 此外,需要使用命令行,设置一个文件名来读写。
/// 检查命令行参数后,通过调用InitProtection辅助方法来初始化数据保护。
/// 这个方法返回一个MySafe类型的对象,嵌入IDataProtector。
/// 之后,根据命令行参数,调用Write或Read方法
/// </summary>
/// <param name="arr"></param>
public static void DataProtectionStart(string[] arr)
{
if (arr.Length != 2 || arr.Intersect(options).Count() != 1)
{
ShowUsage();
return;
}
string fileName = arr[1];
MySafe safe = InitProtection();
switch (arr[0])
{
case writeOption:
Write(safe, fileName);
break;
case readOption:
Read(safe, fileName);
break;
default:
ShowUsage();
break;
}
}
/// <summary>
/// 通过InitProtection方法调用AddDataProtection和ConfigureDataProtection扩展方法,
/// 通过依赖注入添加数据保护,并配置它。
/// AddDataProtection方法通过调用DataProtectionServices.GetDefaultServices静态方法,注册默认服务。
/// ConfigureDataProtection方法包含一个有趣的特殊部分。在这里,它定义了密钥应该如何保存。
/// 示例代码把Directorylnfo实例传递给PersistKeysToFileSystem方法,把密钥保存在实际的目录中。
/// 另一个选择是把密钥保存到注册表(PersistKeysToRegistry)中, 可以创建自己的方法,把密钥保存在定制的存储中。
/// 所创建密钥的生命周期由SetDefaultKeyLifetime方法定义。
/// 接下来,密钥通过调用ProtectKeysWithDpapi来保护。
/// 这个方法使用DPAPI保护密钥,加密与当前用户一起存储的密钥。
/// ProtectKeysWithCeritificate允许使用证书保护密钥。
/// API还定义了UseEphemeralDataProtectionProvider方法,把密钥存储在内存中。
/// 再次启动应用程序时,需要生成新密钥。这个功能非常适合于单元测试
/// </summary>
/// <returns></returns>
private static MySafe InitProtection()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("."))
.SetDefaultKeyLifetime(TimeSpan.FromDays(20))
.ProtectKeysWithDpapi();
IServiceProvider services = serviceCollection.BuildServiceProvider();
return ActivatorUtilities.CreateInstance<MySafe>(services);
}
private static void Write(MySafe safe, string fileName)
{
Console.WriteLine("输入内容,开始write方法");
string content = Console.ReadLine();
string encrypted = safe.Encrypt(content);
File.WriteAllText(fileName, encrypted);
Console.WriteLine($"内容已写入{fileName}");
}
private static void Read(MySafe safe, string fileName)
{
string encrypted = File.ReadAllText(fileName);
string decrypted = safe.Decrypt(encrypted);
Console.WriteLine(decrypted);
}
private static void ShowUsage()
{
Console.WriteLine("Usage: DataProtectionSample options filename");
Console.WriteLine("Options:");
Console.WriteLine("\t-r Read");
Console.WriteLine("\t-w Write");
Console.WriteLine();
}
资源的访问控制
在操作系统中,资源(如文件和注册表键,以及命名管道的句柄)都使用访问控制列表(ACL)来保护。下图显示了这个映射的结构。资源有一个关联的安全描述符。安全描述符包含了资源拥有者的信息,并引用了两个访问控制列表:自由访问控制列表(Discretionary Access Control List,DACL)和系统访问控制列表(System Access Control List,SACL)。DACL用来确定谁有访问权;SACL用来确定安全事件日志的审核规则。ACL包含一个访问控制项(Acces sControl Entries,ACE)列表。ACE包含类型、安全标识符和权限。在DACL中,ACE的类型可以是允许访问或拒绝访问。可以用文件设置和获得的权限是创建、读取、写入、删除、修改、改变许可和获得拥有权。
下面的程序说明了如何从文件中读取访问控制列表。
/// <summary>
/// FileStream类定义了GetAccessControl()方法,该方法返回一个FileSecunty对象。
/// FileSecunty是一个.NET类,它表示文件的安全描述符。
/// FileSecurity类派生自基类ObjectSecurity、CommonObjectSecurity、NativeObjectSecurity和FileSystemSecunty。
/// 其他表示安全描述符的类有CryptoKeySecunty、EventWaitHandleSecurity、MutexSecurrty、
/// RegistrySecurity、SemaphoreSecurity、PipeSecurity和ActiveDirectorySecunty。
/// 所有这些对象都可以使用访问控制列表来保护。
/// 一般情况下,对应的.NET类定义了GetAccessControl()方法,返回相应的安全类;
/// 例如,Mutex.GetAccessControl()方法返回一个MutexSecunty类,
/// PipeStream.GetAccessControI()方法返回一个PipeSecunty类。
///
/// FileSecunty类定义了读取、修改DACL和SACL的方法。
/// GetAccessRules()方法以AuthorizationRuleCollection类的形式返回DACL。
/// 要访问SACL,可以使用GetAuditRules方法。
///
/// 在GetAccessRules()方法中,可以确定是否应使用继承的访问规则(不仅仅是用对象直接定义的访问规则)。
/// 最后一个参数定义了应返回的安全标识符的类型。这个类型必须派生自基类IdentityReference。
/// 可能的类型有NTAccount和Securityldentifier。这两个类都表示用户或组。
/// NTAccount类按名称查找安全对象,Secuntyldentifier类按唯一的安全标识符查找安全对象。
///
/// 返回的AuthorizationRuIeCollection包含AuthoriztionRuIe对象。AuthorizationRuIe对象是ACE的.NET表示。
/// 在这里的例子中,因为访问一个文件,所以AuthorizationRule对象可以强制转换为FileSystemAccessRule类型。
/// 在其他资源的ACE中,存在不同的.NET表示,例如MutexAccessRule和PipeAccessRule。
/// 在FileSystemAccessRuIe类中,AccessConfrolType、FileSystemRights和IdentityReference属性返回ACE的相关信息
/// </summary>
/// <param name="fileName"></param>
public static void FileAccessControlStart(string fileName)
{
using (FileStream stream = File.Open(fileName,FileMode.Open))
{
FileSecurity securityDescriptor = stream.GetAccessControl();
AuthorizationRuleCollection rules = securityDescriptor.GetAccessRules(true, true, typeof(NTAccount));
foreach (AuthorizationRule rule in rules)
{
var fileRule = rule as FileSystemAccessRule;
Console.WriteLine($"Access类型:{fileRule.AccessControlType}");
Console.WriteLine($"Rights:{fileRule.FileSystemRights}");
Console.WriteLine($"Identity:{fileRule.IdentityReference.Value}");
Console.WriteLine();
}
}
}
结果
Access类型:Allow
Rights:FullControl
Identity:NT AUTHORITY\SYSTEM
Access类型:Allow
Rights:FullControl
Identity:BUILTIN\Administrators
Access类型:Allow
Rights:FullControl
Identity:DESKTOP-I1SSGPI\Dream
这里写代码片
使用证书发布代码
可以利用数字证书来对程序集进行签名,让软件的消费者验证软件发布者的身份。根据使用应用程序的地点,可能需要证书。例如,用户利用ClickOnce安装应用程序,可以验证证书,以信任发布者。Microsoft通过Windows Error Reporting,使用证书来找出哪个供应商映射到错误报告。
在商业环境中,可以从Verisign或Thawte之类的公司中获取证书。从软件厂商购买证书(而不是创建自己的证书)的优点是,那些证书可以证明软件的真实性有很高的可信度,软件厂商是可信的第三方。但是,为了测试,.NET提供了一个命令行实用程序,使用它可以创建测试证书。创建证书和使用证书发布软件的过程相当复杂,但是本节用一个简单的示例说明这个过程。
设想有一个名叫ABC的公司。公司的软件产品(simple.exe)应该值得信赖。首先,输入下面的命令,创建一个测试证书:
>makecert -sv abckey.pvk —r —n "CN=ABC Corporation" abccorptest.cer
这条命令为ABC公司创建了一个测试证书,并把它保存到abccorptest.cer文件中。-sv abckey.pvk参数创建一个密钥文件,来存储私钥。在创建密钥文件时,需要输入一个必须记住的密码。
创建证书后,就可以用软件发布者证书测试工具(Cert2spc.exe)创建一个软件发布者测试证书:
>cert2spc abccorptest.spc
有了存储在spc文件中的证书和存储在pvk文件中的密钥文件,就可以用pvk2pfx实用程序创建一个包含证书和密钥文件的pfx文件
>pvk2pfx —pvk abckey.pvk -spc abccorptest.spc —pfx abccorptest.pfx
现在可以用signtooL.exe实用程序标记程序集了。使用sign选项来标记,用-f指定pfx文件中的证书,用-v指定输出详细信息:
>signtool sign -f abccorptest.pfx —V simple.exe
为了建立对证书的信任,可使用证书管理器cermgr或MMC插件Certificates,通过Trusted Root Certification Authorities 和Trusted Publishers 安装它。之后就可以使用signtool验证签名是否成功:
>signtool verify -v -a simple.exe