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

时间:2015-07-13 06:34:24

标签: python amazon-web-services web-applications amazon-s3 amazon-cloudfront

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

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

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

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

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

2 个答案:

答案 0 :(得分:9)

使用签名的Cookie

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

调整RSA签名生成

我可以想象您可能有将要求签名的Cookie作为无效选项的要求。在这种情况下,我尝试通过比较使用boto和RSAcryptography模块来加快签名。另外两个备选选项是m2cryptopycrypto,但对于此示例,我将使用加密。

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

结果显示加密速度更快,但每个签名请求仍需要接近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,但我希望此信息有用。

答案 1 :(得分:5)

简言之

考虑到您的用例的详细信息,考虑您是否可以(除了使用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的实际签名调用中。虽然这有点令人失望,但这是一个明确的指标,我应该考虑实际的签约流程以节省开支。

我刚刚使用为我们生成的2048位密钥CloudFront,因此我决定看看较小密钥对性能的影响。我使用较短的密钥重新运行配置文件:

>>> 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找到了评论。