24 安全性

安全性

为了确保应用程序的安全,安全性有几个重要方面需要考虑。一是应用程序的用户,访问应用程序的是一个真正的用户,还是伪装成用户的某个人?如何确定这个用户是可以信任的?如本章所述,确保应用程序安全的用户方面是一个两阶段过程:用户首先需要进行身份验证,再进行授权,以验证该用户是否可以使用需要的资源。

对于在网络上存储或发送的数据呢?例如,有人可以通过网络嗅探器访问这些数据吗?这里,数据的加密很重要。一些技术,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

猜你喜欢

转载自blog.csdn.net/Star_Inori/article/details/80794727