下面的函数是Request类的一部分。这个类基本上只保存从报头和正文中解析出来的信息,我想实现一个安全的HMAC验证方案。我以前没有这样做过,但我在SO和其他地方都读过很多关于这个主题的文章。我选择sha256算法作为性能和安全性之间的中间道路。
这个类保存了所有变量,除了API_KEY
(一个定义的常量,在每个版本中都会更改)和共享密钥(我在设备注册期间使用公钥加密保护共享密钥的初始三方交换后存储在DB中)。validNonce()
只是在数据库中查找nonce,看看它是否有效。
我的问题归结为:我走对了吗?我错过了什么明显的东西吗?
public function isValidRequest($secret)
{
if(!validNonce($this->nonce))
{
return false;
}
$data = API_KEY . $this->device_key . $this->user_key .
$this->cnonce . $this->nonce . $this->body;
$hmac_hash = hash_hmac("sha256",$data,$secret);
return $this->hash === $hmac_hash;
}
我讲对了吗?我错过了什么明显的东西吗?
计时攻击!好吧,所以它不是真的明显,但这是这里缺失的一块拼图。
一般解决方案(diff格式)为:
- return $this->hash === $hmac_hash;
+ return hash_equals($this->hash, $hmac_hash);
此外,在处理多部分消息时,如何将数据馈送到HMAC需要仔细考虑。否则,即使您安全地使用HMAC,您也会引入从其组成部分的不同组合创建两个相同字符串的风险,这可能会影响安全性。
$data = API_KEY . $this->device_key . $this->user_key .
$this->cnonce . $this->nonce . $this->body;
这是我在设计PASETO时关心的问题,所以我定义了一个方案,我称之为PAE(预认证编码),它使这些消息不同且明确。你可以在这里找到一个实现(转载如下):
<?php
class Util
{
// ~8<~8<~8<~8<~8<~ SNIP SNIP ~8<~8<~8<~8<~8<~
/**
* Format the Additional Associated Data.
*
* Prefix with the length (64-bit unsigned little-endian integer)
* followed by each message. This provides a more explicit domain
* separation between each piece of the message.
*
* Each length is masked with PHP_INT_MAX using bitwise AND (&) to
* clear out the MSB of the total string length.
*
* @param string ...$pieces
* @return string
*/
public static function preAuthEncode(string ...$pieces): string
{
$accumulator = 'ParagonIE_Sodium_Core_Util::store64_le('count($pieces) & PHP_INT_MAX);
foreach ($pieces as $piece) {
$len = Binary::safeStrlen($piece);
$accumulator .= 'ParagonIE_Sodium_Core_Util::store64_le($len & PHP_INT_MAX);
$accumulator .= $piece;
}
return $accumulator;
}
// ~8<~8<~8<~8<~8<~ SNIP SNIP ~8<~8<~8<~8<~8<~
}
和兼容的JavaScript实现:
function LE64(n) {
var str = '';
for (var i = 0; i < 8; ++i) {
if (i === 7) {
// Clear the MSB for interoperability
n &= 127;
}
str += String.fromCharCode(n & 255);
n = n >>> 8;
}
return str;
}
function PAE(pieces) {
if (!Array.isArray(pieces)) {
throw TypeError('Expected an array.');
}
var count = pieces.length;
var output = LE64(count);
for (var i = 0; i < count; i++) {
output += LE64(pieces[i].length);
output += pieces[i];
}
return output;
}
这意味着你想要你的最终结果使用像PAE的东西来编码所有的片段到$data
函数时创建和验证你的HMAC标签。不要忘记使用hash_equals()
来比较两个字符串