为什么我自己*实现*一个已知的、已发布的、被广泛认为是安全的加密算法是错误的?

信息安全 密码学 定制方案
2021-08-29 23:05:40

我知道我们永远不应该设计¹加密算法的一般建议。在本网站和布鲁斯·施奈尔(Bruce Schneier)这样的专业人士的网站上,已经对它进行了广泛的讨论。

然而,一般的建议远不止于此:它说我们甚至不应该实现比我们更聪明的算法,而是坚持由专业人士制作的众所周知的、经过良好测试的实现。

这是我找不到详细讨论的部分。我还简单地搜索了 Schneier 的网站,也找不到这个断言。

因此,为什么我们也被明确建议不要实施加密算法?我非常感谢您参考一位著名的安全专家谈论这个问题的答案。

¹更准确地说,是随心所欲地设计;这可能是一次很好的学习经历;但是拜托拜托,永远不要使用 我们 设计的东西

4个回答

您想避免自己实现加密算法的原因是因为侧信道攻击

什么是边信道?

当您与服务器通信时,消息的内容是通信的“主要”渠道。但是,您可以通过其他几种方式从您的沟通伙伴那里获取信息,这些信息不直接涉及他们告诉您的事情。

这些包括但不限于:

  • 回复你的时间
  • 服务器处理您的请求所消耗的能量
  • 服务器访问缓存以响应您的频率。

什么是侧信道攻击?

简而言之,边信道攻击是对涉及这些边信道之一的系统的任何攻击。以下面的代码为例:

public bool IsCorrectPasswordForUser(string currentPassword, string inputPassword)
{
    // If both strings don't have the same length, they're not equal.
    if (currentPassword.length != inputPassword.length)
        return false;

    // If the content of the strings differs at any point, stop and return they're not equal.
    for(int i = 0; i < currentPassword.length; i++)
    {
        if (currentPassword[i] != inputPassword[i])
            return false;
    }

    // If the strings were of equal length and never had any differences, they must be equal.
    return true;
}

这段代码在功能上似乎是正确的,如果我没有打错任何字,那么它可能会做它应该做的事情。你还能发现侧信道攻击向量吗?这是一个演示它的示例:

假设用户的当前密码是Bdd3hHzj(8 个字符)并且攻击者正试图破解它。如果攻击者输入一个相同长度的密码,则if检查和循环的至少一次迭代都for将被执行;但如果输入的密码短于或长于 8 个字符,则只会if执行 。前一种情况做的工作更多,因此比后者需要更多的时间来完成;比较检查 1-char、2-char、3-char 等密码所需的时间很简单,并注意 8 个字符是唯一显着不同的字符,因此可能是正确的长度密码。

有了这些知识,攻击者就可以改进他们的输入。首先他们尝试aaaaaaaa通过aaaaaaaZ,每个都只执行一次循环迭代for但是当它们来到 时Baaaaaaa,会发生两次循环迭代,这再次比以任何其他字符开头的输入需要更多的时间来运行。这告诉攻击者用户密码的第一个字符是字母B,他们现在可以重复此步骤来确定剩余的字符。

这与我的加密代码有何关系?

加密代码看起来与“常规”代码非常不同。查看上面的示例时,它似乎没有任何明显的错误。因此,在您自己实现事物时,可能并不明显的是,执行它应该做的代码只是引入了一个严重的缺陷。

我能想到的另一个问题是程序员不是密码学家。他们倾向于以不同的方式看待世界,并且经常做出可能很危险的假设。例如,看下面的单元测试:

public void TestEncryptDecryptSuccess()
{
    string message = "This is a test";
    KeyPair keys = MyNeatCryptoClass.GenerateKeyPair();    

    byte[] cipher = MyNeatCryptoClass.Encrypt(message, keys.Public);
    string decryptedMessage = MyNeatCryptoClass.Decrypt(cipher, keys.Private);

    Assert.Equals(message, decryptedMessage);
}

你能猜出是怎么回事吗?我不得不承认,这不是一个公平的问题。MyNeatCryptoClass实现 RSA,如果没有明确给出指数,则在内部设置为使用默认指数 1。

是的,如果您使用 1 的公共指数,RSA 就可以正常工作。它不会真正“加密”任何东西,因为“x 1 ”仍然是“x”。

您可能会问自己,谁在他们的正常头脑中会这样做,但确实有这种情况发生

实施错误

实现自己的代码可能出错的另一个原因是实现错误。正如用户Bakuridu在评论中指出的那样,与其他错误相比,加密代码中的错误是致命的。这里有一些例子:

心脏出血

当涉及到密码学时, Heartbleed可能是最著名的实现错误之一。虽然不直接涉及加密代码的实现,但它仍然说明了一个相对“小”的错误会发生多么可怕的错误。

虽然链接的 Wikipedia 文章对这个问题有更深入的了解,但我想让 Randall Munroe 比以往任何时候都更简洁地解释这个问题:

https://xkcd.com/1354/ https://xkcd.com/1354/ - 图像在 CC 2.5 BY-NC 下获得许可

Debian 弱 PRNG 错误

早在 2008 年, Debian中就有一个 bug,它影响了所有进一步使用的密钥材料的随机性。Bruce Schneier 解释了 Debian 团队所做的更改以及为什么会出现问题。

基本要点是检查 C 代码中可能存在的问题的工具抱怨使用未初始化的变量。虽然通常这是一个问题,但用本质上随机的数据播种 PRNG 也不错。然而,由于没有人喜欢盯着警告并且被训练忽略警告可能会导致它自己的问题,因此“违规”代码在某个时候被删除,从而导致 OpenSSL 使用的熵减少。

概括

总而言之,除非它旨在成为一种学习体验,否则不要实施您自己的 Crypto!使用经过审查的加密库,旨在使正确操作变得容易,错误操作变得困难。因为 Crypto 很容易出错。

提到的侧信道攻击是一件大事。我会更概括一下。您的加密库是非常高风险/高难度的代码。这通常是受信任库,可以保护其他软件系统的其余部分。这里的错误很容易达到数百万美元。

更糟糕的是,您经常不得不与自己的编译器作斗争。在许多不同的编译器下,对这些算法的可信实现进行了大量研究,并进行了一些小的调整,以确保编译器不会做坏事。多么糟糕?好吧,考虑一下每个人都提到的那些侧信道攻击。您仔细编写代码以避免所有这些攻击,并且做对了所有事情。然后你在它上面运行编译器。编译器不知道你在做什么。它可以轻松查看您为避免侧通道攻击所做的一些事情,查看更快的方法,并优化您精心添加的代码!这甚至出现在内核代码中,其中一个明显无序的分配最终会授予编译器优化错误检查的权限!

检测这样的事情只能用反汇编程序来完成,而且要有很大的耐心。

哦,永远不要忘记编译器错误。上个月,我花了一周的大部分时间来追踪我的代码中的一个错误,它实际上非常好——这是我的编译器中的一个已知错误,它实际上是导致问题的原因。现在我很幸运,因为这个错误使我的程序崩溃了,所以每个人都知道需要做些什么。一些编译器错误更加微妙。

反对推出自己的加密货币的理由是,即使面对广泛的测试,错误也可以毫无症状地隐藏在加密软件中。

一切似乎都运行良好。例如,在签名/验证应用程序中,验证者将确定有效签名并拒绝无效签名。签名本身看起来就像胡言乱语。但漏洞仍然存在,等待实际攻击。

您是否曾经在代码中打错字符而没有注意到,导致编辑器突出显示或快速编译或运行时错误,然后及时修复?如果它没有突出显示,编译和运行时没有明显的症状,你会发现那个错字吗?这就是滚动您自己的加密货币时遇到的问题。

即使在不可能进行侧信道攻击的情况下,密码算法也经常具有对安全至关重要但并不明显的实现细节。两个例子:

  • ECDSA 签名算法生成签名时需要使用随机整数。对于使用给定私钥生成的每个签名,此整数需要不同。如果重复使用,任何获得两个签名的人都可以使用基本的模运算来恢复私钥。(索尼在 PlayStation 3 的版权保护上犯了这个错误,每个签名都使用相同的数字。)

  • 为RSA 算法生成密钥对需要生成两个随机的大素数。在正常情况下,恢复私钥需要整数分解或解决RSA 问题,这两种数学运算都很慢。但是,如果一个密钥对与另一个密钥对共享它的一个素数,那么只需计算两个公钥的最大公约数,就可以很容易地恢复这两个密钥对的私钥。(许多路由器在第一次上电时生成 SSL 证书,此时没有太多可用的随机性。有时,两个路由器会碰巧生成具有重叠密钥对的证书。)