我应该使用 HMAC 摘要的 Base64 还是仅使用 HMAC 十六进制摘要?
Should I use Base64 of HMAC digest or just HMAC hex digest?
图例
我公开了一个 API,它要求客户端通过发送两个 header 来签署请求:
Authorization: MyCompany access_key:<signature>
Unix-TimeStamp: <unix utc timestamp in seconds>
要创建签名部分,客户端应使用我的 API 服务颁发的密钥。
在 Python (Py3k) 中它看起来像:
import base64
import hmac
from hashlib import sha256
from datetime import datetime
UTF8 = 'utf-8'
AUTH_HEADER_PREFIX = 'MyCompany'
def create_signature(access_key, secret_key, message):
new_hmac = hmac.new(bytes(secret_key, UTF8), digestmod=sha256)
new_hmac.update(bytes(message, UTF8))
signature_base64 = base64.b64encode(new_hmac.digest())
return '{prefix} {access_key}:{signature}'.format(
prefix=AUTH_HEADER_PREFIX,
access_key=access_key,
signature=str(signature_base64, UTF8).strip()
)
if __name__ == '__main__':
message = str(datetime.utcnow().timestamp())
signature = create_signature('my access key', 'my secret key', message)
print(
'Request headers are',
'Authorization: {}'.format(signature),
'Unix-Timestamp: {}'.format(message),
sep='\n'
)
# For message='1457369891.672671',
# access_key='my access key'
# and secret_key='my secret key' will ouput:
#
# Request headers are
# Authorization: MyCompany my access key:CUfIjOFtB43eSire0f5GJ2Q6N4dX3Mw0KMGVaf6plUI=
# Unix-Timestamp: 1457369891.672671
我想知道是否可以避免将字节的编码摘要处理为 Base64,而只使用 HMAC.hexdigest()
来检索字符串。
这样我的函数就会变成:
def create_signature(access_key, secret_key, message):
new_hmac = hmac.new(bytes(secret_key, UTF8), digestmod=sha256)
new_hmac.update(bytes(message, UTF8))
signature = new_hmac.hexdigest()
return '{prefix} {access_key}:{signature}'.format(
prefix=AUTH_HEADER_PREFIX,
access_key=access_key,
signature=signature
)
但后来我发现 Amazon uses similar approach 在我的第一个代码片段中:
Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );
看到 Amazon 不使用十六进制摘要,我停止了自己继续使用它,因为也许他们知道我不知道的事情。
更新
我测量了性能并发现十六进制摘要更快:
import base64
import hmac
import string
from hashlib import sha256
UTF8 = 'utf-8'
MESSAGE = '1457369891.672671'
SECRET_KEY = 'my secret key'
NEW_HMAC = create_hmac()
def create_hmac():
new_hmac = hmac.new(bytes(SECRET_KEY, UTF8), digestmod=sha256)
new_hmac.update(bytes(MESSAGE, UTF8))
return new_hmac
def base64_digest():
return base64.b64encode(NEW_HMAC.digest())
def hex_digest():
return NEW_HMAC.hexdigest()
if __name__ == '__main__':
from timeit import timeit
print(timeit('base64_digest()', number=1000000,
setup='from __main__ import base64_digest'))
print(timeit('hex_digest()', number=1000000,
setup='from __main__ import hex_digest'))
结果:
3.136568891000934
2.3460130329913227
问题 #1
有人知道为什么他们坚持使用 Base64 字节摘要而不只使用十六进制摘要吗?是否有充分的理由继续使用这种方法而不是十六进制摘要?
问题 #2
根据RFC2716 Authorization
header 值的格式使用基本身份验证
是:
Authorization: Base64(username:password)
所以基本上你用 Base64 包装两个由冒号分隔的值(用户 ID 和密码)。
正如您在我的代码片段和亚马逊的文档中看到的那样,我也没有看到,亚马逊也不会为 Authorization
header 的自定义值这样做。
将整对包装成 Base64(access_key:signature)
以更贴近此 RFC 是更好的样式还是根本不重要?
亚马逊 在 Signature Version 4 中使用十六进制摘要。
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
您的示例来自签名版本 2,这是较旧的算法,它确实使用 Base-64 编码进行签名(并且在最新的 AWS 区域中也不支持)。
因此,您担心 AWS 知道您不知道的事情是错误的,因为他们的新算法使用它。
在 Authorization:
header 中,除了几个额外的八位字节外,它确实没有什么不同。
Base-64 变得混乱的地方是在查询字符串中传递签名时,因为 +
和(取决于你问的是谁)/
和 =
需要特殊处理-- 它们需要 url-escaped ("percent-encoded") 分别为 %2B
、%2F
和 %3D
...服务器上的变体...或者您必须要求使用 non-standard Base-64 字母表,其中 +
/
=
变为 -
~
_
the way CloudFront does it。 (这个特殊的 non-standard 字母表只是多个 non-standard 选项中的一个,所有 "solving" 与 Base-64 的 URL 中魔术字符的相同问题)。
选择 hex-encoding。
您几乎不可避免地会发现 would-be 您的 API 消费者认为 Base-64 是 "difficult."
图例
我公开了一个 API,它要求客户端通过发送两个 header 来签署请求:
Authorization: MyCompany access_key:<signature>
Unix-TimeStamp: <unix utc timestamp in seconds>
要创建签名部分,客户端应使用我的 API 服务颁发的密钥。
在 Python (Py3k) 中它看起来像:
import base64
import hmac
from hashlib import sha256
from datetime import datetime
UTF8 = 'utf-8'
AUTH_HEADER_PREFIX = 'MyCompany'
def create_signature(access_key, secret_key, message):
new_hmac = hmac.new(bytes(secret_key, UTF8), digestmod=sha256)
new_hmac.update(bytes(message, UTF8))
signature_base64 = base64.b64encode(new_hmac.digest())
return '{prefix} {access_key}:{signature}'.format(
prefix=AUTH_HEADER_PREFIX,
access_key=access_key,
signature=str(signature_base64, UTF8).strip()
)
if __name__ == '__main__':
message = str(datetime.utcnow().timestamp())
signature = create_signature('my access key', 'my secret key', message)
print(
'Request headers are',
'Authorization: {}'.format(signature),
'Unix-Timestamp: {}'.format(message),
sep='\n'
)
# For message='1457369891.672671',
# access_key='my access key'
# and secret_key='my secret key' will ouput:
#
# Request headers are
# Authorization: MyCompany my access key:CUfIjOFtB43eSire0f5GJ2Q6N4dX3Mw0KMGVaf6plUI=
# Unix-Timestamp: 1457369891.672671
我想知道是否可以避免将字节的编码摘要处理为 Base64,而只使用 HMAC.hexdigest()
来检索字符串。
这样我的函数就会变成:
def create_signature(access_key, secret_key, message):
new_hmac = hmac.new(bytes(secret_key, UTF8), digestmod=sha256)
new_hmac.update(bytes(message, UTF8))
signature = new_hmac.hexdigest()
return '{prefix} {access_key}:{signature}'.format(
prefix=AUTH_HEADER_PREFIX,
access_key=access_key,
signature=signature
)
但后来我发现 Amazon uses similar approach 在我的第一个代码片段中:
Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );
看到 Amazon 不使用十六进制摘要,我停止了自己继续使用它,因为也许他们知道我不知道的事情。
更新
我测量了性能并发现十六进制摘要更快:
import base64
import hmac
import string
from hashlib import sha256
UTF8 = 'utf-8'
MESSAGE = '1457369891.672671'
SECRET_KEY = 'my secret key'
NEW_HMAC = create_hmac()
def create_hmac():
new_hmac = hmac.new(bytes(SECRET_KEY, UTF8), digestmod=sha256)
new_hmac.update(bytes(MESSAGE, UTF8))
return new_hmac
def base64_digest():
return base64.b64encode(NEW_HMAC.digest())
def hex_digest():
return NEW_HMAC.hexdigest()
if __name__ == '__main__':
from timeit import timeit
print(timeit('base64_digest()', number=1000000,
setup='from __main__ import base64_digest'))
print(timeit('hex_digest()', number=1000000,
setup='from __main__ import hex_digest'))
结果:
3.136568891000934
2.3460130329913227
问题 #1
有人知道为什么他们坚持使用 Base64 字节摘要而不只使用十六进制摘要吗?是否有充分的理由继续使用这种方法而不是十六进制摘要?
问题 #2
根据RFC2716 Authorization
header 值的格式使用基本身份验证
是:
Authorization: Base64(username:password)
所以基本上你用 Base64 包装两个由冒号分隔的值(用户 ID 和密码)。
正如您在我的代码片段和亚马逊的文档中看到的那样,我也没有看到,亚马逊也不会为 Authorization
header 的自定义值这样做。
将整对包装成 Base64(access_key:signature)
以更贴近此 RFC 是更好的样式还是根本不重要?
亚马逊 在 Signature Version 4 中使用十六进制摘要。
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
您的示例来自签名版本 2,这是较旧的算法,它确实使用 Base-64 编码进行签名(并且在最新的 AWS 区域中也不支持)。
因此,您担心 AWS 知道您不知道的事情是错误的,因为他们的新算法使用它。
在 Authorization:
header 中,除了几个额外的八位字节外,它确实没有什么不同。
Base-64 变得混乱的地方是在查询字符串中传递签名时,因为 +
和(取决于你问的是谁)/
和 =
需要特殊处理-- 它们需要 url-escaped ("percent-encoded") 分别为 %2B
、%2F
和 %3D
...服务器上的变体...或者您必须要求使用 non-standard Base-64 字母表,其中 +
/
=
变为 -
~
_
the way CloudFront does it。 (这个特殊的 non-standard 字母表只是多个 non-standard 选项中的一个,所有 "solving" 与 Base-64 的 URL 中魔术字符的相同问题)。
选择 hex-encoding。
您几乎不可避免地会发现 would-be 您的 API 消费者认为 Base-64 是 "difficult."