HTTPS之加密方式

776 阅读33分钟

何谓安全?

对于信息安全性的重要性,我想大家都不会否认。那么具体来说应该具有哪些特性才能称之为安全呢?举个简单的例子:我给你发送一条消息“借给我100元”,当你收到这条消息并且处理后你的账户里面会少出来100块,我的账户会多出来100块。在这个过程中,你是消息接收方,我是消息发送方。

1.作为通信双方的你我都不希望让其他人能读懂这条消息,这是信息的机密性,即消息在传递过程中不被其他人解读。

2.作为通信双方的你我都不希望消息内容变成"借老子1000块!"(操,借钱还这么牛逼,100块都不给你,还要1000块!死去...),这是信息的完整性,即可以校验出信息在传送过程中是否被篡改。

3.作为消息接收方的你需要确认是不是真正的我给你发的借钱的消息吧,会不会是个诈骗犯要骗我100块!这是对信息的认证,即接收者要可以验证消息的发送者确实是自己希望的发送者。

4.作为消息接收方的你肯定不希望在借给了我100块之后,我耍无赖失口否认说没借过你钱,这是信息的不可否认性,即消息发送者不可以否认说这个信息不是我发送的。

总结来说,在通信过程中,满足这4个特征:机密性,完整性,认证,不可否认性,就可以认为信息是安全的。那么接下来的几个小节来介绍一下有那些工具可以使得我们在传递消息的时候具有以上4个特征。

对称密码-对称密钥

英文:Symmetric Cryptography

对称密码加密可以保障信息的机密性。举一个简单的例子,一把锁,两把相同的钥匙,就是对称密码;即:使用相同的密钥来加密和解密。没有密钥的其他人是无法解读信息的真正内容是什么的。常见的两个对称加密标准有DESAES

DES是一种对称密钥加密算法,在1976年被美国联邦政府的国家标准局确定为联邦资料处理标准,随后在国际上广泛流传开来。它基于使用56位密钥的对称算法。现在现在已经不是一种安全的加密方法,主要因为它使用的56位密钥过短。后来又发展出了3DES(即执行三次DES加密)。由于DES已经不再安全,后来又推出了新的对称加密标准AES,采用的算法为Rijndael。算法的具体实现逻辑这里不去解释,这里关注的是如何利用它们(即,保障信息的机密性的手段)。看一下简单的加密解密的数学公式:

  1. 加密:encrypted_data = encrypt_function(message, key)
  2. 解密:message = decrypt_function(encrypted_data, key)

C#使用AES的代码如下:

/// <summary>
///  AES加密
/// </summary>
/// <param name="key">128bit,192bit,125bit</param>
/// <returns></returns>
public static byte[] AESEncrypt(this byte[] value, byte[] key)
{
    //todo 参数检查
    using (var symmetricAlgorithm = Aes.Create())
    {
        symmetricAlgorithm.Key = key;
        symmetricAlgorithm.Mode = CipherMode.ECB;
        symmetricAlgorithm.Padding = PaddingMode.PKCS7;
        //加密
        using (var encryptor = symmetricAlgorithm.CreateEncryptor())
        {
            return encryptor.TransformFinalBlock(value, 0, value.Length);
        }
    }
}

/// <summary>
///  AES解密
/// </summary>
/// <param name="key">128bit,192bit,125bit</param>
/// <returns></returns>
public static byte[] AESDecrypt(this byte[] value, byte[] key)
{
    //todo 参数检查
    using (var symmetricAlgorithm = Aes.Create())
    {
        symmetricAlgorithm.Key = key;
        symmetricAlgorithm.Mode = CipherMode.ECB;
        symmetricAlgorithm.Padding = PaddingMode.PKCS7;
        //解密
        using (var encryptor = symmetricAlgorithm.CreateDecryptor())
        {
            return encryptor.TransformFinalBlock(value, 0, value.Length);
        }
    }
}

static void Main()
{
    var value = "lnh".ToBytes(Encoding.UTF8);
    //构造128bit的key,guid正好是128,权且当作key了。
    var key = Guid.NewGuid().ToByteArray();
    var encryptedData = value.AESEncrypt(key);
    var decryptedData = encryptedData.AESDecrypt(key);
    var decryptedDataString = Encoding.UTF8.GetString(decryptedData);
    Console.WriteLine();
}

遗留问题

密钥配送问题:共享的密钥如何交到接受消息方的手上呢?双方可以事先共同约定一个密钥,但是这种办法是无法满足互联网规模的需要的,互联网规模的环境是假设通信双方事先并不知道对方的存在,怎么事先约定呢,行不通吧。

下面接下来的公钥密钥可以解决这个问题。

公钥密码-非对称密钥

英文:Asymmetric Cryptography

对称密码加密可以解决信息的机密性的问题,但是却无法提供双方如何才能得到加密所用密钥的途径。我们回到最初的目的想一想,我们想要的机密性的核心在于别人无法取得信息的真实内容,也就是解密;而如何生成这个机密的信息,其实并不是我们关注的点,你能生成,他能生成,都没区别,只要我控制住只有我才能解密,那么机密性的问题就解决了。所以解决密钥配送的问题的关键就在于,把密钥分成两部分,一个加密用,一个解密用,它们总是成对出现的。配送的是加密用的密钥(也叫公钥),解密用的叫私钥,这个只有我自己知道,不会在任何地方传输,那么也就不存在配送的问题了。

其实很多计算机中的问题都是无解的,往往却又是有解决办法的,它的解决办法其实并不是直接的解决这个问题,而是规避掉这个问题,使得它不在是一个问题的。比如密钥配送的问题,如果说我们有安全的方式解决密钥配送的问题,直接使用这个安全的方式配送我们想要传递的信息不就是了,我们还绕个弯配送密钥干什么呢。公钥密码其实并未解决密钥配送的问题,而是使得它不再是个问题,即:公钥可以公开给任何人,不再需要保密(本质上来说,密钥和待加密的信息同样重要),而是通过控制解密来达到我们想要的机密性,绕过了如何机密的配送密钥的问题。

公钥密码就是这么一个简单的原理:公钥(=public key)加密私钥(=private key)解密,它可以保障信息的机密性,同时解决密钥的配送问题。那么这个时候通信双方的流程就是这样的:

  1. 发送方向接收方请求一个public key
  2. 发送方使用 public key 加密消息, public key 和加密的消息泄露都没关系,因为只有 private key 才能解密;
  3. 接收方用 private key 解密消息。

至于如何产生出来这样一对 public keyprivate key 以及相对于的加密解密算法,这其中涉及到很复杂的数学问题,这里就不展开介绍了(笔者也不懂...)。我们看一下最广泛使用的公钥密码算法RSA在C#里面怎么使用吧:

/// <summary>
///  RSA加密
/// </summary>
/// <param name="publicKey">公钥</param>
/// <returns></returns>
public static byte[] RSAEncrypt(this byte[] value, string publicKey)
{
    //todo 参数检查
    using (var asymmetricAlgorithm = new RSACryptoServiceProvider())
    {
        asymmetricAlgorithm.FromXmlString(publicKey);
        return asymmetricAlgorithm.Encrypt(value,false);
    }
}

/// <summary>
///  RSA解密
/// </summary>
/// <param name="privateKey">私钥</param>
/// <returns></returns>
public static byte[] RSADecrypt(this byte[] value, string privateKey)
{
    //todo 参数检查
    using (var asymmetricAlgorithm = new RSACryptoServiceProvider())
    {
        asymmetricAlgorithm.FromXmlString(privateKey);
        return asymmetricAlgorithm.Decrypt(value,false);
    }
}

static void Main()
{
    string privateKey;
    string publicKey;
    using (var asymmetricAlgorithm = RSA.Create())
    {
        privateKey = asymmetricAlgorithm.ToXmlString(true);
        publicKey = asymmetricAlgorithm.ToXmlString(false);
    }

    var value = "lnh".ToBytes(Encoding.UTF8);
    //公钥加密
    var encryptedData = value.RSAEncrypt(publicKey);
    //私钥解密
    var decryptedData = encryptedData.RSADecrypt(privateKey);

    var decryptedDataString = Encoding.UTF8.GetString(decryptedData);
    Console.WriteLine();
}

.Net库中已经提供了公钥密码相关的类,开箱即用:

image

对公钥密钥的攻击

中间人攻击:这钟类型的攻击发生在上述流程中的第一步,即发送方A向接收方B请求 public key 的时候。这时有一个拦路打劫的家伙M,截获了这个 public key ,自己据为己有。然后M把自己的一个 public key 给到了A,A是浑然不觉,傻乎乎的用这个假的 public key 加密了信息,发送了出去,这时候M拦截到了这个消息,用自己的 private key 解密了这个消息,然后篡改一番,用真正的 public key 进行加密,发给了B。这个时候B以为是A发送的,A也以为自己发给了B,其实都被M给玩了...文字可能不是很清晰,看图:

image

遗留问题

公钥的认证问题:公钥密钥可以解决规避掉的配送问题,但是新问题又来了,**这个公钥真的是你的吗?**针对上述的中间人攻击,其实我们发现,获取公钥的这一方并不能确认自己收到的公钥就是自己真正请求的那一方提供的。这个问题先放一放(后续会介绍),下面先看看保障信息的完整性方面有那些工具可用。

密码散列函数

英文:Cryptographic hash function

密码散列函数可以保障的信息完整性,用来校验要传递的信息是否被篡改过。比如通常在下载文件的时候,官方的网站上都会列出来其MD5或者SHA1的值来校验。它的工作原理和要求大致如下:

输入一组数据message,然后得到一组简短的数据hash,只要是采用相同的算法,输入message就能得到hash:hash = hash_function (message); 其过程是不可逆的,你不能由hash得出message; 满足message的微小变化会(比如只改动1字节)会使得hash产生巨大的变化(就好比两个双胞胎,各处都很像,但是他们的指纹却不是相同的); 两组不同的消息message1message2,不能得出相同的hash(理论上可以,只是要尽可能的使这个过程困难)。 常用的密码散列函数(算法)有Message Digest Algorithm以及Secure Hash Algorithm

MD5

英文:Message Digest Algorithm 5

中文明为消息摘要算法第五版,这也说明其实它也有前面几个版本,比如MD4(这里就不介绍了)。MD5算法是输入任意长度的数据(Message),然后算出固定长度的数据 16byte=128bits ,用16进制表示这16个byte就是32位。C#使用MD5的代码如下:

 /// <summary>
 /// MD5摘要算法
 /// </summary>
 /// <param name="value"></param>
 /// <returns>128 bits,16 byte array</returns>
 public static byte[] ToMD5(this byte[] value)
 {
     if (value == null || value.Length == 0)
     {
         throw new ArgumentNullException(nameof(value));
     }
     using (var hashAlgorithm = MD5.Create())
     {
         return hashAlgorithm.ComputeHash(value);
     }
 }

再一次指出,md5的结果是固定的16byte=128bits ,用16进制表示是32个字符。网上由很多的16进制16个字符的md5,其实这都不是完整的md5,只是截取了32位中的16位而已。

SHA

英文:Secure Hash Algorithm

从使用者的角度来看,MD5SHA没有什么本质区别,差异在于其算法的实现方式,生成的hash的长度,其抗攻击破解的难度不一样。此外由于SHA的强度比MD5要大,所以在计算SHA的时候,所消耗的资源(时间,空间都有)也会比MD5要多。即使如此,现在MD5(128bit)和SHA-1(160bit)均已遭到了破解:news.cnblogs.com/n/563589/。S…SHA256的代码如下:

/// <summary>
/// SHA256哈希算法
/// </summary>
/// <returns>256 bits,32 byte array</returns>
public static byte[] ToSHA256(this byte[] value)
{
    if (value == null || value.Length == 0)
    {
        throw new ArgumentNullException(nameof(value));
    }
    using (var hashAlgorithm = SHA256.Create())
    {
        return hashAlgorithm.ComputeHash(value);
    }
}

NET的库已经帮我们封装好了密码散列函数相关的类,开箱即用。

image

密码散列函数的实际应用

  1. 检查文件是否被修改:上面一开始举得例子下载文件的例子。
  2. 基于口令的加密:通常我们在存储用户的密码的时候,都会采用这种方式(除非你是csdn),一般还会辅助的加上盐。
  3. 消息认证码:后面介绍到到。
  4. 数字签名:后面会介绍到。
  5. 伪随机数生成器:后面会介绍到。

针对密码散列函数的攻击

  • 强碰撞性攻击:比如上面提到的Google破解了SHA-1,即使用大量的计算来找出两个数据message不一样,但是hash值却一样的过程,如果找到了这样的两块数据,那么再使用这个hash作为数据的完整性校验的手段,其实已经没有意义了。解决办法是升级SHA-2,增大计算出这样两块数据的难度。

  • 暴力破解:比如网上很多的MD5的解密,其实原理在于他们有大量的MD5 hash库,比如 123456 的MD5是 e10adc3949ba59abbe56e057f20f883e ,那么在你给我一个 e10adc3949ba59abbe56e057f20f883e 的时候,我就知道你的原文是 123456 。解决办法是加盐,增大这种暴力比对的难度。

针对上面两种攻击方式都是在于增加破解难度,使其在现有的计算能力下不能轻易的被攻破,没有绝对的安全,只是相对上来说是安全的,当破解你带来的收益要低于其破解成本的时候,你才是安全的

遗留问题

hash被篡改了:比如上面下载文件的时候官方会给出MD5或者SHA1的hash值,这里我们假设一下,官方提供hash值的渠道被黑掉了,给了你一个篡改过的hash值,然后你下载了一个被篡改过的文件,你是分辨不出来的。其实我们下载文件,然后比对官方给的hash值,这里是假设官方的hash值是没有被篡改的。

那么接下来的消息认证码MAC是可以解决这个问题。

消息认证码

英文:Message Authentication Code

消息认证码(MAC)的作用就是在保障完整性的基础上,同时提供认证(认证=消息是来自真正的发送者)的功能,用来解决上述密码散列函数遗留的问题。可以简单的这样理解,MAC是在密码散列函数+共享密钥后算出的hash值,由于密钥是只有通信双方才知道的,那么就可以认为通过MAC得到的hash可以保障信息的完整性以及同时提供认证的能力。这里我们假设双方不存在密钥配送的问题(即双方已经持有相同的密钥,至于是通过什么方式传递的,这里先不关心)。

使用密码散列函数可以实现MAC,这种方式称为HMAC(Hash Message Authentication Code):tools.ietf.org/html/rfc210…en.wikipedia.org/wiki/Hash-b… = mac_function (message,key)。C#中使用HMAC的代码如下:

/// <summary>
/// HMACSHA1算法
/// </summary>
/// <returns>160 bits,20 byte array</returns>
public static byte[] ToHMACSHA1(this byte[] value,byte[] key)
{
    if (value == null || value.Length == 0)
    {
        throw new ArgumentNullException(nameof(value));
    }
    if (key == null || key.Length == 0)
    {
        throw new ArgumentNullException(nameof(key));
    }
    using (var macAlgorithm =new HMACSHA1())
    {
        macAlgorithm.Key = key;
        return macAlgorithm.ComputeHash(value);
    }
}

static void Main()
{
    var value = "lnh".ToBytes();
    var key = "123".ToBytes();
    var mac = value.ToHMACSHA1(key);
    Console.WriteLine();
}

.Net类库中开箱即用的MAC相关的类,开箱即用:

image

消息认证码的实际应用

  • SWIFT:此SWIFT非苹果的Swift语言,而是Society for Worldwide Interbank Financial(环球银行金融电信协会)的简写。在银行之间进行传递交易消息时,会用到MAC来确认消息的完整性以及对消息进行认证。在没有使用公钥密码进行密钥交换之前,消息认证码使用的共享密钥时靠人力通过11路来完成的。
  • IPsec:增强版Ip协议,用来认证和校验消息的完整性。
  • SSL/TLS:后续会介绍到。

针对消息认证码的攻击

  • 重放攻击:比如你给我转账100元,携带了mac的消息,其实我并不用破解你的消息和mac,原封不动拿你的消息重复给我转账就是了,你信不信我可以把你账户里面所有的钱都变成我的...解决办法是对消息添加编号和时间戳,使得消息接收方针对这个消息只处理一次。
  • 密钥推测攻击:和密码散列码的暴力攻击是类似的,不再细说。

遗留问题

消息不是我发送的,是你自己伪造的:基于MAC的原理是在于通信双方共享密钥,那么消息接收方可以判断消息是来自真正的发送者,但是却无法向第三者证明这一点,为什么呢?因为消息的接收方也有密钥啊,消息发送者完全可以说这是消息接收者自己用这个共享密钥生成的消息,毕竟密钥双方都有。

那么接下来的数字签名是可以解决这个问题。

数字签名(Digital Signature)

上面的MAC可以保障信息的完整性,同时具有提供消息认证的能力,但是又遗留了一个可以否认消息是我发送的问题。究其原因在于通信双方使用了同一个密钥来生成MAC,你说是他生成的,他说是你生成的。那么怎么解决呢,其实也简单,双方使用不同的密钥;消息发送方使用签名密钥生成一个“签名”(就像签字画押按手印一样的道理,表示我承认这些信息是我发送的),消息接收方使用另外验证密钥来验证这个签名,这其实就是数字签名。

数字签名对签名密钥验证密钥进行了区分,验证密钥无法生成签名;此外签名密钥只能由签名人持有,而验证密钥则可以由任何想要验证签名的人持有。回想一下,这个签名密钥和验证密钥是不是感觉似曾相识,对了,和上面我们提到的公钥密码中的公钥和私钥非常类似吧

公钥密码:密钥分为加密密钥和解密密钥,用加密密钥无法进行解密;解密密钥只有需要解密的人持有,而加密密钥则是任何需要加密的人都可以持有。

实际上,数字签名和公钥密钥有着非常紧密的联系,简单点来说,数字签名是通过把公钥密码“反过来用”来实现的:

私钥 / 签名密钥 公钥 / 验证密钥
公钥密码 接收者解密时使用 发送者加密时使用
数字签名 签名者生成签名时使用 验证者验证签名时使用
谁持有密钥 个人持有 只要需要,任何人都可以持有

数字签名的实现是:签名人用私钥加密一段信息来生成签名,验证者使用公钥来解密这个签名,如果可以解密成功,则说明验证成功。觉得很奇怪是不是?为什么能用公钥解密就证明签名验证通过了呢?其实这是由于私钥和密钥是成对出现的(具有严密的数学关系),只有公钥才能解密与之配对的私钥加密的信息,那么既然能够解密,那么这个消息肯定是持有私钥的这一方生成的。你估计还会想到一个问题,公钥是公开的呀,你有我由他也有,那么私钥生成的这个加密的签名大家都可以解密,根本没有机密性啊。是的,这样理解是完全正确的,私钥加密的信息是不具备机密性的;这是因为数字签名是用来提供消息的不可否认性的,它并不关心机密性的问题。

上面我们说到“签名人用私钥加密{一段信息}来生成签名”。那么问题来了,这{一段信息}是什么信息?关于这一段信息我们由两种选择:1是消息本身,2是消息的hash。

下图是对消息本身进行签名的过程:

image

实际中我们一般采用的是对消息的hash进行签名的方式,因为消息本身可能非常大,加密解密过程会非常消耗资源。再C#中使用RSA来实现数字签名:

/// <summary>
/// 数字签名
/// </summary>
/// <returns></returns>
public static byte[] DigitalSignature(this byte[] value, string privateKey)
{
    using (var asymmetricAlgorithm = new RSACryptoServiceProvider())
    {
        asymmetricAlgorithm.FromXmlString(privateKey);
        return asymmetricAlgorithm.SignData(value, SHA1.Create());
    }
}

/// <summary>
/// 数字签名验证
/// </summary>
/// <returns></returns>
public static bool DigitalSignatureVerify(this byte[] value, string publicKey,byte[] digitalSignature)
{
    using (var asymmetricAlgorithm = new RSACryptoServiceProvider())
    {
        asymmetricAlgorithm.FromXmlString(publicKey);
        return asymmetricAlgorithm.VerifyData(value, SHA1.Create(), digitalSignature);
    }
}

static void Main()
{
    string privateKey;
    string publicKey;
    using (var asymmetricAlgorithm = RSA.Create())
    {
        privateKey = asymmetricAlgorithm.ToXmlString(true);
        publicKey = asymmetricAlgorithm.ToXmlString(false);
    }

    var value = "lnh".ToBytes(Encoding.UTF8);

    //用私钥生成数字签名
    var digitalSignature = value.DigitalSignature(privateKey);

    //用公钥验证数字签名
    var verified = value.DigitalSignatureVerify(publicKey, digitalSignature);

    Console.WriteLine();
}

数字签名本身的实现是使用了公钥密钥相关的算法。

数字签名的实际应用

  • 公钥证书:上面在介绍公钥密码的时候,遗留的一个公钥认证的问题,即我们怎么才能知道自己拿到的公钥是不是真正的公钥,而不是被第三方伪造的呢?可以把公钥当作消息,然后施加数字签名,所得到的就是公钥证书,关于证书的知识后续博客会介绍。
  • SSL/TLS:SSL/TLS在认证服务器是否合法的时候会使用服务器证书,就是上面提到的公钥证书;于此相对,服务器在对客户端进行认证的时候,也会使用客户端证书。关于SSL/TLS后续博客会介绍。

针对数字签名的攻击

  • 中间人攻击:在公钥密码这一小节中提到了中间人攻击,因为数字签名其实就是使用它来实现的,那么对于数字签名来说,中间人攻击也是具有相同的威胁。
  • 对密码散列函数的攻击:数字签名使用了密码散列函数,那么数字签名也面临同样的威胁。
  • 利用数字签名攻击公钥密钥:这块好复杂,笔者研究明白再补充( ╯□╰ )。。。

遗留问题

数字签名可以识别出篡改和伪装,还可以防止否认,也就是说数字签名可以提供信息安全中的完整性、认证和不可否认性这3点的保障(很强大有木有)。然而这一切都基于一个假设“公钥必须是真正的发送者提供的”,和公钥密钥陷入了同一个问题。我们发现自己陷入了一个死循环:数字签名可以用来识别篡改、伪装以及否认的,但是为此我们又需要从一个没有被伪装的真正的发送者那里得到一个没有被篡改的密钥......这是一个鸡生蛋蛋生鸡的问题。

06

细心的读者或许可以看出来,上面我们的加密、散列、mac,签名也好,消费的数据都是byte[],而byte[]是不方便书写、打印、复制和粘贴的,下面看一看byte[]编码的问题。换换脑子,鸡生蛋还是蛋生鸡的问题放一放先。

伪随机数生成器

英文:Pseudo-Random Number Generator

随机数大家不陌生,但是随机数怎么就和信息安全扯上关系了呢?其实想一想我们在给自己的账号设置密码的时候,是不是都会尽量的让其他人不会轻易的猜到我们的密码,虽然并不是随机,但是它就像是满足了随机数的一个特征:不可预测性。那么对于信息安全来说来说,也是用到了这个特定,当然还有随机数的随机性,不可重复性这两点特征。

随机性:完全杂乱的序列,没有统计学偏差; 不可预测性:不能由已经得到的随机数才猜测出下一个随机数是什么; 不可重复性:不能生成重复的随机数。 根据生成的随机数是否满足这3点要求(1<2<3,依次增强)。大致可以划分伪弱伪随机数,强伪随机数,真随机数(强度依次增大)。

随机性 不可预测性 不可重复性
弱伪随机数 × × 只具备随机性
强伪随机数 × 同时具备不可预测性
真随机数 同时具备不可重复性

仅仅依靠软件我们是无法生成真随机数的,这里我们只关注以下伪随机数(即强伪随机数的生成,可用于密码学安全)的生成,比如一个典型的生成器如下:

11

生成器自己维护一个内部状态,同时接受一个随机数的种子,来生成具体的随机数。具体是实现方式有利用密码散列函数(单向性支撑了不可预测性)、利用加密密钥作为随机数的种子的一部分(密钥的机密性支持了不可预测性)等等。

在C#可以使用的伪随机数生成方式:

//1. Random
 var random = new Random();
 var random1 = random.Next(100);
 Console.WriteLine(random1);
 
 //2. Guid 
 var random2 = Guid.NewGuid().ToString("N");
 Console.WriteLine(random2);
 
 //3. RNGCryptoServiceProvider
 RandomNumberGenerator randomNumberGenerator = new RNGCryptoServiceProvider();
 var random3Bytes = new byte[32];
 randomNumberGenerator.GetBytes(random3Bytes);
 var random3 = random3Bytes.ToHexString();
 Console.WriteLine(random3);

一般情况下,Guid即可满足要求(但是只有固定的16byte),如需更高强度的伪随机数,可以使用 RNGCryptoServiceProvider 来生成任意长度的随机数。

伪随机数的实际应用

  • 生成密钥:对称密码和消息认证码;
  • 生成密钥对:公钥密钥和数字签名;
  • 生成nonce:防止重放攻击;
  • 生成salt:盐用于增强基于口令的密码的加密。

针对伪随机数生成器的攻击

伪随机数的程序结构可以说很简单,但是其中的每个环节都有可能成为被攻击的突破口。

  • 对种子的攻击:如果暴露了种子,那么其实攻击者就可以得到其所有的伪随机数(假设攻击者知道其内部算法的情况下)。
  • 对伪随机数池的攻击:如果我们实现生成了一大堆的伪随机数,用的时候从里面取一个,那么这个存储这些预先生成的伪随机数的地方,就可能会被泄露。

混合密码系统

针对密码相关的基本工具介绍就暂时可以告一段落了,回顾总结以下有这6个**(对称密码、公钥密钥、密码散列函数、消息认证码、数字签名、伪随机数生成器)**基本工具,下面我们用这6个基本工具来组合一些高级的工具出来。

上一篇中介绍到了对称密码(比如AES)和公钥密码(比如RSA),公钥密码解决了对称密码的密钥配送问题(其实是绕过了)。

那么如果作为接收方我想要回复发送方的消息怎么办?按照公钥密码的机制,我是不能用自己的私钥加密信息发出去的,因为拥有持有我的公钥的任何人都是可以解密这个信息的。所以,如果仅使用公钥密码,那么就需要通信双方都持有对方的公钥+自己的私钥。这个成本是很高昂的,首先公钥加密解密的速度是会比对称密码加密低2~3个数量级,也就是几百倍的差异;其次双方都面临着针对公钥的认证问题(防止中间人攻击)。

那么我们可以结合之前提到的一些工具,组合一下,来得到一个性价比高的加密通信方式,即使用以下三个基本工具,组合一个高级点的工具(同时具备对称密码和公钥密码的优点):

  1. 对称密码:因为其加密解密的速度比较快,所消耗的资源相对来说比较小,所以用来在通信中承担真正的加密和解密任务。
  2. 公钥密码:用来传递对称密码所需的密钥。
  3. 伪随机数生成器:用来生成对称密码的密钥。

看一下混合密码的加密过程:

12

上图是用伪随机数生成器生成一个加密用的会话密钥,来加密明文;同时,把这个会话密钥作为公钥密码中的明文,用公钥加密;然后把这两个密文组合在一起,同时发送给接收方。这里的公钥密钥起到的是一个保证会话密钥机密性的作用,并未直接用来加密真正的明文(又想起来一句话:计算机科学的中任何问题,都可以通过添加一个中间层来解决,此言不虚;在另外一个[认证授权]系列的博客中,笔者也有这样的体会)。来看以下接收方解密的流程:

13

相对于加密过程来说,是完全反过来的一个过程,就不再解释了。看一段C#中实际使用的代码:

static void Main()
{
    string privateKey;
    string publicKey;
    using (var asymmetricAlgorithm = RSA.Create())
    {
        privateKey = asymmetricAlgorithm.ToXmlString(true);
        publicKey = asymmetricAlgorithm.ToXmlString(false);
    }
    // 发送者加密
    var hybridCiphertext = HybridEncrypt(publicKey, "lnh-明文");
    Console.WriteLine(hybridCiphertext);

    // 接收者解密
    var plaintext = HybridDecrypt(privateKey, hybridCiphertext);
    Console.WriteLine(plaintext);

    Console.ReadKey();
}

static string HybridEncrypt(string publicKey, string plaintext)
{
    var plaintextBytes = plaintext.ToBytes(Encoding.UTF8);

    //1. 生成伪随机数,作为会话签名
    var sessionKey = SecurityHelper.BuildPseudoRandomNumber(16);

    //2. 使用sessionKey作为AES的密钥进行加密
    var ciphertextBytes = plaintextBytes.AESEncrypt(sessionKey);

    //3. 使用公钥对会话密钥进行加密
    var sessionkeyCiphertextBytes = sessionKey.RSAEncrypt(publicKey);

    //4. 模拟合成的消息
    var hybridCiphertext = ciphertextBytes.ToHex() + "." + sessionkeyCiphertextBytes.ToHex();

    return hybridCiphertext;
}

static string HybridDecrypt(string privateKey, string hybridCiphertext)
{
    //1. 分离合成的密文
    var ciphertext = hybridCiphertext.Split('.')[0];
    var sessionkeyCiphertext = hybridCiphertext.Split('.')[1];

    var ciphertextBytes = ciphertext.HexToBytes();
    var sessionkeyCiphertextBytes = sessionkeyCiphertext.HexToBytes();

    //2. 用私钥解密得到会话密钥
    var sessionkey = sessionkeyCiphertextBytes.RSADecrypt(privateKey);

    //3. 用会话密码解密
    var plaintextBytes = ciphertextBytes.AESDecrypt(sessionkey);

    return plaintextBytes.GetString(Encoding.UTF8);
}

混合密码系统的实际应用

  • SSL/TLS:最常见的一个应用场景了,后续会介绍。

遗留问题

混合密码系统只能说是降低了单纯的公钥密码带来的成本问题,而公钥密码遗留的公钥认证问题,在混合密码系统中依然存在。同时使用了伪随机数生成器,混合密码系统也会面临针对伪随机生成器的一些攻击。

证书(Certificate)- 为公钥添加数字签名

数字签名遗留下问题,汇总在一起的核心就是验证公钥必须是真正的发送者提供的

所以,想要解决这个问题单靠纯粹的技术手段是行不通了,我们陷入了一个死循环,因此引入了一个社会学中的信任体系来转移我们所面临的问题,即证书以及相关体系结构,提供逐级的信任保障。我们先看看证书是一个什么东西,以及证书的这套相关体系如何提供这种“信任”保障的。

我们从一出生就会和各种各样的证书打交道,比如出生证,学生证,身份证,驾照,学位证等等,它们都有一个共同点,就是有你本人的真实信息以及开具证明的机构的盖章。那么在需要提供证明你就是你的地方,出具这个证书即可,如果对方不信任你的证书,则可以到开具证书的机构来校验。假如你提供一个假证,而对方没有严格的审查的话,或许你是可以蒙混过关的。

计算机领域的证书和现实社会中的各种证书的工作原理是完全一样的,因为其工作在计算机体系中,也被称为“数字证书”。计算机中数字证书是这样定义的:由证书授权中心进行数字签名的,包含公钥以及其拥有者信息的一个文件。注意:证书的真正用途在于为公钥提供认证功能,所以有时候也叫做公钥证书。我们使用这个被称做证书的文件来转移我们在信息安全层面所面临的死循环的问题,为什么说是转移而不是解决呢,这是因为你拿到一个证书后,也需要进行校验吧,而校验又需要一个真正的发送者提供的公钥才行,那么你就需要另外一个证书来保障,然后你就会一直的循环下去,,,这也是为什么在计算机体系中有根证书的存在,以及相关的证书授权认证中心会是一个层级的关系,这就是为了在你不信任一个证书的时候,可以继续往上一个层级来寻求验证,直到根证书。那么问题就来了,假如你也不相信根证书怎么办?这其实是一个无法回答的问题,笔者想起来之前读《人类简史》的时候,有一个至今烙印在脑海中的观点:“如今的社会,是一个由想象所构建的秩序”。其实想一想,也确实是如此。比如你为何相信国家的存在呢,为何会把钱存进银行呢。你拿出来一张毛爷爷(从物理的角度来看,它就是一张纸而已)为什么就能从饭店买来一堆食物,这其实就是你相信它,对方也相信它,所有人都相信它,背后有银行体系为其担保,那么什么为银行提供担保呢,背后有我们的国家提供保障,这就是一个信任的体系。计算机体系的数字证书也是基于这么一个共同的想象所构建的信任秩序。补充一个新闻:Google 宣布将完全取消对沃通和 StartCom 所有证书的信任(news.cnblogs.com/n/573409/),…

PKI

英文全称:Public Key Infrastructure

证书得以运行的这个基本的体系称为PKI(Public Key Infrastructure),即公钥基础设施,它是一套以公钥密码为核心的技术规范标准的一个代名词。总体来说,PKI由3个要素组成:PKI的消费者;认证机构(Certificate Authority,简称CA);证书仓库。我们常说的CA证书,就是由CA机构签名颁发的证书。CA负责生成密钥对(也可由用户提供)、负责对用户身份进行认证、负责生成并颁发证书、负责作废证书(en.wikipedia.org/wiki/Certif… CA的证书是自签名的(也就是自己证明自己),其下级机构逐层签名,构成一个金字塔似的结构。当然你平时自己也可以生成自签名的证书,但是除了你自己,其他地方是不认可你这个证书的(就好比你拿一张白纸,写上这是100块,然后别人就相信你它值100块吗?),想要得以正常运行,是需要用户主动确认表示认可你这个证书才行。比如我们用Fiddler抓取HTTPS的内容的时候,其实Fiddler自己生成了一个自签名的根证书,然后你主动的确认信赖它,只有这样,证书构造的这个验证链才能得以正常运行。想起来12306就自己搞了一个自签名的证书,想必大家都有印象吧,,,需要自己下载下来证书,然后导入到计算机中,再确认信任它;其实这也是一个很尴尬的事情,全球最大的几家CA清一色不是美国就是俄罗斯。

公钥证书包含的信息

好了,分析完数字证书这套体系为什么能够运转起来为我们提供公钥的认证的保障之后。看看计算机中的公钥证书是什么样的,我们拿https://www.google.com的做例子(F12,打开安全选项卡即可),证书的相关信息如下:

证书除了包含公钥、签名算法和证书的层级结构(比如google这个的顶级颁发机构是GeoTurst Global CA),还有一些证书的序号,版本信息,有效时间等等。这些信息由一个证书数据格式的标准规范来规定的,一个很通用的格式是x509,感兴趣的可以了解一下,这里就不介绍了。C#中有很多X509相关的类可以供我们使用。比如来读取一下上述google的这个证书信息(我把证书导出为了base64格式的数据,可以方便的直接包含在代码中):

private static readonly string GoogleBase64FormatPublicKeyCertificate = @"
-----BEGIN CERTIFICATE-----
MIIEgDCCA2igAwIBAgIIaCtCibL6TxQwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTcwNjIxMTQzNTUwWhcNMTcwOTEzMTM1MzAw
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFHtwc
1ECzMNuXPshUwS9IkyKPuHYA6WXa3ohXW/wMHo0IKnu5WgXmduLS6cGoFlT3oq3P
PXJz11gKpdBJeoLs/g4lG3mOnGRSQbjtsWsXCPsunMjeq0vTfidJ2Gt+1eMHh5B4
qcgOxbXEK9AE6GZGCL3MSV2lE2oG0GDpStZkLhKt11GE+qrLSQCpH9XgzknHdrvz
OU6Kl3e5W+4QO6rTq5285D18Ep6Cugf39JbZQZHSu0ejLnmtSOHwUg1i/vbJrDN/
yVwEySn+drxv0CzPDrTMiqGLVxBOSwN9wU9cRphiLLSdE4Sy2p77jCNLWzbcQQ5P
5f+2hLXb2Z/N1kAZAgMBAAGjggFLMIIBRzAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE
XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0
MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G
A1UdDgQWBBTxnCXke+dTXQNZyBd5gWpjj8kdKjAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMCEGA1UdIAQaMBgwDAYKKwYBBAHW
eQIFATAIBgZngQwBAgIwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29n
bGUuY29tL0dJQUcyLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAgHtFBvVyIQwRSrUC
RbPu0fZFl9HJCJ0FBXVlQl0JO5PdRTtXlkfDqeoZcE3l562/FotKyaPKRyhktYDb
5tnYo74q1gKxfmTjXEtkBeUDUAlNzepuXYudu43A5athR/GPIDxXQvQc4Lakmafi
LJFTZLw7ZjmkU0mkt3uaiUXTuOiA+5hjjGLzFzRpRXvUcqIggGUTVJ4v7HSmOl3x
tjePNc8ps3bivp8WtB4jR6k+PvVmlYDN/Uf7+cwmOEtrXUBCrVwA/LL+j3mkwHK0
49h5xyjmB/ndmH/HgjY2DSzu2HMekkPJEnPWmkxqRP2c08UqQoUbXE9zdL35Ys5A
JRO+1w==
-----END CERTIFICATE-----
";
static void Main()
{
    var bytes = GoogleBase64FormatPublicKeyCertificate.ToBytes(Encoding.ASCII);
    var x509 = new X509Certificate2(bytes);
    var text = x509.ToString(true);
    Console.WriteLine(text);
    Console.ReadKey();
}

PKCS

英文全称:Public Key Cryptography Standards

证书的相关格式以及交换标准在PKCS(Public-Key Cryptography Standards)中有详细的定义。常见的证书编码格式:

  • PEM :Privacy Enhanced Mail,以"-----BEGIN..."开头,以 "-----END..."结尾,中间内容是base64编码的文本。
  • DER :Distinguished Encoding Rules,二进制格式。

常见到的几个证书扩展名:

  • .p7b :特点是其包含相关其证书链,不含私钥。
  • .cer / .crt :一般是采用DER编码的二进制格式,不含私钥。
  • .pem :一般是采用PEM编码的base64格式,不含私钥,另外其文件一般采用ASCII编码。
  • .pfx / .p12 :一般是采用DER编码的二进制格式,包含公钥和私钥的。

参考 & 引用

密码工具箱-blackheart

书籍