如何应对生成签名 URL 以通过 CloudFront 访问私有内容的性能?

Posted

技术标签:

【中文标题】如何应对生成签名 URL 以通过 CloudFront 访问私有内容的性能?【英文标题】:How to cope with the performance of generating signed URLs for accessing private content via CloudFront? 【发布时间】:2015-10-01 07:51:39 【问题描述】:

AWS S3 和 CloudFront 的一个常见用例是提供私有内容。常见的解决方案是使用签名的 CloudFront URL 来访问使用 S3 存储的私有文件。

但是,这些 URL 的生成是有代价的:使用私钥计算任何给定 URL 的 RSA 签名。对于 Python(或 boto,AWS 的 Python SDK),rsa (https://pypi.python.org/pypi/rsa) 库用于此任务。在我 2014 年末的 MBP 中,使用 2048 位密钥每次计算大约需要 25 毫秒。

此成本可能会影响使用此方法授权通过 CloudFront 访问私有内容的应用程序的可扩展性。想象一下,多个客户端以 25~30ms/req 频繁地请求访问多个文件。

在我看来,签名计算本身并没有太大的改进,尽管上面提到的 rsa 库最近一次更新是在大约 1.5 年前。我想知道是否有其他技术或设计可以优化此过程的性能以实现更高的可扩展性。还是我们只需要投入更多的硬件并尝试以蛮力的方式解决它?

一项优化可以使 API 端点在每个请求中接受多个文件签名并批量返回签名的 URL,而不是在单独的请求中单独处理它们,但计算所有这些签名所需的总时间仍然存在。

【问题讨论】:

我自己今天在想这个。您是从字面上测量签名的生成,还是加载密钥、准备策略等任务? @abathur,我只测量了生成签名 (RSA) 的时间(密钥已经缓存在服务器端,并且策略已经配置)和整个请求处理的时间(包括. 一些自定义逻辑)。所以在我的机器上,签名生成大约需要 25 毫秒,处理整个请求大约需要 30 毫秒。 @MLister:您能否提供一个代码块,显示您用于计算示例 URL 的 RSA 签名的行? @Peque,即使用 boto 中提供的函数的单行代码,即 CloudFront 分配对象上的 create_signed_url 函数:boto.readthedocs.org/en/latest/ref/…。此外,分发对象在启动时创建一次并缓存在服务器上,而不是在每次请求时重新创建。 【参考方案1】:

使用签名的 Cookies

当我将 CloudFront 与许多私有 URL 一起使用时,我更喜欢在满足所有 restrictions 时使用 Signed Cookies。这不会加快签名 cookie 的生成速度,但会将签名请求的数量减少到每个用户一个,直到它们过期。

调整 RSA 签名生成

我可以想象您可能有将签名 cookie 呈现为无效选项的要求。在这种情况下,我尝试通过比较与 boto 和 cryptography 一起使用的 RSA 模块来加快签名速度。另外两个替代选项是m2crypto 和pycrypto,但对于本例,我将使用密码学。

为了测试使用不同模块对 URL 进行签名的性能,我减少了 _sign_string 方法以删除除字符串签名之外的任何逻辑,然后创建了一个新的 Distribution 类。然后我从boto tests 获取私钥和​​示例 URL 进行测试。

结果表明,加密速度更快,但每个签名请求仍需要接近 1 毫秒。这些结果因 iPython 在时序中使用范围变量而产生了更高的偏差。

timeit -n10000 rsa_distribution.create_signed_url(url, message, expire_time)
10000 loops, best of 3: 6.01 ms per loop

timeit -n10000 cryptography_distribution.create_signed_url(url, message, expire_time)
10000 loops, best of 3: 644 µs per loop

完整的脚本:

from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

import rsa

from boto.cloudfront.distribution import Distribution

from textwrap import dedent

# The private key provided in the Boto tests
pk_key = dedent("""
    -----BEGIN RSA PRIVATE KEY-----
    MIICXQIBAAKBgQDA7ki9gI/lRygIoOjV1yymgx6FYFlzJ+z1ATMaLo57nL57AavW
    hb68HYY8EA0GJU9xQdMVaHBogF3eiCWYXSUZCWM/+M5+ZcdQraRRScucmn6g4EvY
    2K4W2pxbqH8vmUikPxir41EeBPLjMOzKvbzzQy9e/zzIQVREKSp/7y1mywIDAQAB
    AoGABc7mp7XYHynuPZxChjWNJZIq+A73gm0ASDv6At7F8Vi9r0xUlQe/v0AQS3yc
    N8QlyR4XMbzMLYk3yjxFDXo4ZKQtOGzLGteCU2srANiLv26/imXA8FVidZftTAtL
    viWQZBVPTeYIA69ATUYPEq0a5u5wjGyUOij9OWyuy01mbPkCQQDluYoNpPOekQ0Z
    WrPgJ5rxc8f6zG37ZVoDBiexqtVShIF5W3xYuWhW5kYb0hliYfkq15cS7t9m95h3
    1QJf/xI/AkEA1v9l/WN1a1N3rOK4VGoCokx7kR2SyTMSbZgF9IWJNOugR/WZw7HT
    njipO3c9dy1Ms9pUKwUF46d7049ck8HwdQJARgrSKuLWXMyBH+/l1Dx/I4tXuAJI
    rlPyo+VmiOc7b5NzHptkSHEPfR9s1OK0VqjknclqCJ3Ig86OMEtEFBzjZQJBAKYz
    470hcPkaGk7tKYAgP48FvxRsnzeooptURW5E+M+PQ2W9iDPPOX9739+Xi02hGEWF
    B0IGbQoTRFdE4VVcPK0CQQCeS84lODlC0Y2BZv2JxW3Osv/WkUQ4dslfAQl1T303
    7uwwr7XTroMv8dIFQIPreoPhRKmd/SbJzbiKfS/4QDhU
    -----END RSA PRIVATE KEY-----""")

# Initializing keys in a global context
cryptography_private_key = serialization.load_pem_private_key(
    pk_key,
    password=None,
    backend=default_backend())


# Instantiate a signer object using PKCS 1v 15, this is not recommended but required for Amazon
def sign_with_cryptography(message):
    signer = cryptography_private_key.signer(
        padding.PKCS1v15(),
        hashes.SHA1())

    signer.update(message)
    return signer.finalize()


# Initializing the key in a global context
rsa_private_key = rsa.PrivateKey.load_pkcs1(pk_key)


def sign_with_rsa(message):
    signature = rsa.sign(str(message), rsa_private_key, 'SHA-1')

    return signature


# All this information comes from the Boto tests.
url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes"
expected_url = "http://d604721fxaaqy9.cloudfront.net/horizon.jpg?large=yes&license=yes&Expires=1258237200&Signature=Nql641NHEUkUaXQHZINK1FZ~SYeUSoBJMxjdgqrzIdzV2gyEXPDNv0pYdWJkflDKJ3xIu7lbwRpSkG98NBlgPi4ZJpRRnVX4kXAJK6tdNx6FucDB7OVqzcxkxHsGFd8VCG1BkC-Afh9~lOCMIYHIaiOB6~5jt9w2EOwi6sIIqrg_&Key-Pair-Id=PK123456789754"
message = "PK123456789754"
expire_time = 1258237200


class CryptographyDistribution(Distribution):
    def _sign_string(
            self,
            message,
            private_key_file=None,
            private_key_string=None):
        return sign_with_cryptography(message)


class RSADistribution(Distribution):
    def _sign_string(
            self,
            message,
            private_key_file=None,
            private_key_string=None):
        return sign_with_rsa(message)


cryptography_distribution = CryptographyDistribution()
rsa_distribution = RSADistribution()

cryptography_url = cryptography_distribution.create_signed_url(
    url,
    message,
    expire_time)

rsa_url = rsa_distribution.create_signed_url(
    url,
    message,
    expire_time)

assert cryptography_url == rsa_url == expected_url, "URLs do not match"

结论

虽然加密模块在此测试中表现更好,但我建议尝试找到一种利用签名 cookie 的方法,但我希望这些信息有用。

【讨论】:

谢谢。我正在研究选项,昨晚没有足够的时间来测试cryptography 版本。想我会添加一些我的时序图,以防他们以后对某人有所帮助:cryptography 签名方法为 ~1550µs (1.5ms),rsa 签名方法为 ~37ms(与现有 s3 的 ~30µs 相比)签名方法。) @erik-e 感谢您的回答。我稍微研究了签名的 cookie 方法,但似乎 boto 目前不支持它。你自己做的吗? 是的,但使用了与boto 相同的_sign_string 方法。由于时间不那么重要,我没有更改 boto 中的默认 rsa 实现。 请注意,此答案中使用的密钥长度不是 2048 位(最近推荐),因此所达到的速度将比我们使用 2048 位密钥快得多。使用 2048 位密钥时,使用 cryptography 库的实现在基准测试期间每个签名消耗大约 2.5 毫秒。 很好,这也是我的绩效指标出现偏差的原因。【参考方案2】:

简述

考虑你是否可以(除了使用python-cryptography,根据@erik-e)使用更短的密钥长度(可能是change keys more frequently),考虑到你的用例的细节。虽然我可以使用在 ~1550µs 内生成的 2048 位密钥 AWS 进行签名,但在 1028 位时只需要 ~307µs,在 768 位时只需要 ~184µs,在 512 位时只需要 ~113µs。

说明

在对此进行了一些研究之后,我将朝着另一个方向前进,并以@erik-e 给出的(已经很棒的)答案为基础。在我开始之前我应该​​提到我不知道这个想法有多可接受;我只是在报告它对性能的影响(请参阅帖子末尾的问题,了解我在安全 SE 寻求对此的意见时提出的问题)。

我正在收集@erik-e 建议的使用cryptography 签名的时间,并且由于它与我们现有的 S3 签名方法之间仍然存在很大的性能差距,我决定分析代码以查看它是否看起来像可能有什么明显的咀嚼时间:

>>> cProfile.runctx('[sign_url_cloudfront2("...") for x in range(0,100)]', globals(), locals(), sort="time")
         9403 function calls in 0.218 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      200    0.161    0.001    0.161    0.001 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign
      100    0.006    0.000    0.186    0.002 rsa.py:214(_finalize_pkey_ctx)
     1200    0.004    0.000    0.008    0.000 isinstance
      400    0.004    0.000    0.007    0.000 api.py:212(new)
      100    0.003    0.000    0.218    0.002 views.py:888(sign_url_cloudfront2)
      300    0.002    0.000    0.004    0.000 abc.py:128(__instancecheck__)
      100    0.002    0.000    0.008    0.000 hashes.py:53(finalize)
      200    0.002    0.000    0.005    0.000 gc_weakref.py:10(build)
      100    0.002    0.000    0.007    0.000 hashes.py:15(__init__)
      100    0.002    0.000    0.018    0.000 rsa.py:151(__init__)
      100    0.002    0.000    0.014    0.000 hashes.py:68(__init__)
      200    0.002    0.000    0.003    0.000 gc_weakref.py:14(remove)
      200    0.002    0.000    0.003    0.000 api.py:239(cast)
      100    0.002    0.000    0.190    0.002 rsa.py:207(finalize)
      200    0.001    0.000    0.007    0.000 api.py:325(gc)
      500    0.001    0.000    0.001    0.000 getattr
      400    0.001    0.000    0.001    0.000 _cffi_backend.newp
      400    0.001    0.000    0.001    0.000 api.py:150(_typeof)
      200    0.001    0.000    0.002    0.000 api.py:266(buffer)
      200    0.001    0.000    0.001    0.000 utils.py:18(<lambda>)
      300    0.001    0.000    0.001    0.000 _weakrefset.py:68(__contains__)
      200    0.001    0.000    0.001    0.000 _cffi_backend.buffer
      100    0.001    0.000    0.002    0.000 hashes.py:49(update)
      100    0.001    0.000    0.010    0.000 hashes.py:102(finalize)
      100    0.001    0.000    0.003    0.000 hashes.py:88(update)
      200    0.001    0.000    0.001    0.000 method 'encode' of 'str' objects
      100    0.001    0.000    0.019    0.000 rsa.py:528(signer)
      300    0.001    0.000    0.001    0.000 len
      100    0.001    0.000    0.001    0.000 base64.py:42(b64encode)
      100    0.001    0.000    0.008    0.000 backend.py:148(create_hash_ctx)
      200    0.001    0.000    0.001    0.000 _cffi_backend.cast
      200    0.001    0.000    0.001    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_get_digestbyname
      100    0.001    0.000    0.001    0.000 method 'format' of 'str' objects
      100    0.001    0.000    0.003    0.000 rsa.py:204(update)
      200    0.000    0.000    0.000    0.000 method 'pop' of 'dict' objects
      100    0.000    0.000    0.000    0.000 binascii.b2a_base64
      200    0.000    0.000    0.000    0.000 _cffi_backend.typeof
      100    0.000    0.000    0.000    0.000 time.time
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestUpdate
        1    0.000    0.000    0.218    0.218 <string>:1(<module>)
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestInit_ex
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_new
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_free
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestFinal_ex
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_create
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_rsa_padding
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_size
      100    0.000    0.000    0.000    0.000 method 'translate' of 'str' objects
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_cleanup
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_signature_md
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign_init
      100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_destroy
        1    0.000    0.000    0.000    0.000 range
        1    0.000    0.000    0.000    0.000 method 'disable' of '_lsprof.Profiler' objects

虽然signer 中可能会节省一些小成本,但绝大多数时间都花在了 finalize() 调用中,而且几乎所有时间都花在了对 openssl 的实际签名调用中。虽然这有点令人失望,但它清楚地表明我应该关注实际的签约过程以节省开支。

我只是使用 CloudFront 为我们生成的 2048 位密钥,所以我决定看看较小的密钥会对性能产生什么影响。我使用较短的密钥重新运行了配置文件:

>>> cProfile.runctx('[sign_url_cloudfront2("...") for x in range(0,100)]', globals(), locals(), sort="time")
        9203 function calls in 0.063 seconds

  Ordered by: internal time

  ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     100    0.008    0.000    0.008    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign
     400    0.005    0.000    0.008    0.000 api.py:212(new)
     100    0.004    0.000    0.033    0.000 rsa.py:214(_finalize_pkey_ctx)
    1200    0.004    0.000    0.008    0.000 isinstance
     100    0.003    0.000    0.063    0.001 views.py:897(sign_url_cloudfront2)
     300    0.002    0.000    0.004    0.000 abc.py:128(__instancecheck__)
     100    0.002    0.000    0.008    0.000 hashes.py:53(finalize)
     200    0.002    0.000    0.005    0.000 gc_weakref.py:10(build)
     100    0.002    0.000    0.007    0.000 hashes.py:15(__init__)
     100    0.002    0.000    0.014    0.000 hashes.py:68(__init__)
     100    0.002    0.000    0.018    0.000 rsa.py:151(__init__)
     200    0.002    0.000    0.003    0.000 gc_weakref.py:14(remove)
     100    0.001    0.000    0.036    0.000 rsa.py:207(finalize)
     200    0.001    0.000    0.003    0.000 api.py:239(cast)
     200    0.001    0.000    0.006    0.000 api.py:325(gc)
     500    0.001    0.000    0.001    0.000 getattr
     200    0.001    0.000    0.002    0.000 api.py:266(buffer)
     400    0.001    0.000    0.001    0.000 _cffi_backend.newp
     400    0.001    0.000    0.001    0.000 api.py:150(_typeof)
     100    0.001    0.000    0.010    0.000 hashes.py:102(finalize)
     200    0.001    0.000    0.002    0.000 utils.py:18(<lambda>)
     300    0.001    0.000    0.001    0.000 _weakrefset.py:68(__contains__)
     100    0.001    0.000    0.002    0.000 hashes.py:88(update)
     100    0.001    0.000    0.001    0.000 hashes.py:49(update)
     200    0.001    0.000    0.001    0.000 method 'encode' of 'str' objects
     200    0.001    0.000    0.001    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_get_digestbyname
     100    0.001    0.000    0.001    0.000 base64.py:42(b64encode)
     100    0.001    0.000    0.008    0.000 backend.py:148(create_hash_ctx)
     100    0.001    0.000    0.019    0.000 rsa.py:520(signer)
     200    0.001    0.000    0.001    0.000 _cffi_backend.buffer
     200    0.001    0.000    0.001    0.000 method 'pop' of 'dict' objects
     200    0.001    0.000    0.001    0.000 _cffi_backend.cast
     100    0.001    0.000    0.001    0.000 method 'format' of 'str' objects
     100    0.001    0.000    0.001    0.000 time.time
     100    0.001    0.000    0.003    0.000 rsa.py:204(update)
     200    0.000    0.000    0.000    0.000 len
     200    0.000    0.000    0.000    0.000 _cffi_backend.typeof
     100    0.000    0.000    0.000    0.000 binascii.b2a_base64
     100    0.000    0.000    0.000    0.000 method 'translate' of 'str' objects
       1    0.000    0.000    0.063    0.063 <string>:1(<module>)
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestUpdate
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_new
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestInit_ex
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_destroy
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_DigestFinal_ex
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_create
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_sign_init
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_size
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_MD_CTX_cleanup
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_free
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_signature_md
     100    0.000    0.000    0.000    0.000 _Cryptography_cffi_a269d620xd5c405b7.EVP_PKEY_CTX_set_rsa_padding
       1    0.000    0.000    0.000    0.000 range
       1    0.000    0.000    0.000    0.000 method 'disable' of '_lsprof.Profiler' objects

正如我在对 erik-e 的回答的评论中提到的,我看到使用带有 cryptography 模块的 2048 位密钥的完整签名方法的运行时间约为 1550µs。使用 512 位密钥重复相同的测试可将运行时间降低到约 113µs(与我们的 S3 签名方法的约 30µs 相比差了一点)。

这个结果似乎很有意义,但它取决于how acceptable it is to use a shorter key for your purpose。我找到了 3 月份关于 Mozilla 问题报告 suggesting a 512-bit key could be broken for $75 in 8 hours on EC2 的评论。

【讨论】:

你知道cryptography模块的模数是2还是3 prime factors? ​ @RickyDemer 我不会假装知道;到目前为止,我对他们的代码的阅读表明答案将是:无论 openssl 使用什么。如果我关注,我认为这意味着答案是 2。

以上是关于如何应对生成签名 URL 以通过 CloudFront 访问私有内容的性能?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用签名的 URL 将文件上传到谷歌云存储桶

通过 AWS 开发工具包创建签名的 S3 和 Cloudfront URL

如何使用 CloudFront 签名器和 Python Boto 客户端生成预签名的 PUT URL(如果可能)?

当我想通过 PHP 从 YouTube 获取直接 URL 时的签名问题

如何使用 amazon sdk 为虚域生成预签名的 Amazon S3 url?

如何使用云功能通过签名下载地址删除存储图像?