services node elastic ec2 aws application python amazon-web-services web-applications amazon-s3 amazon-cloudfront

python - node - ¿Cómo lidiar con el rendimiento de generar URL firmadas para acceder a contenido privado a través de CloudFront?



elastic beanstalk (2)

Use cookies firmadas

Cuando uso CloudFront con muchas URL privadas, prefiero usar Cookies firmadas cuando se cumplen todas las restricciones . Esto no acelera la generación de cookies firmadas, pero reduce el número de solicitudes de firma para ser uno por usuario hasta que expiren.

Tuning RSA Signature Generation

Me imagino que puede tener requisitos que rinden cookies firmadas como una opción no válida. En ese caso, traté de acelerar la firma comparando el módulo RSA utilizado con boto y criptografía . Dos opciones alternativas adicionales son m2crypto y pycrypto, pero para este ejemplo usaré la criptografía.

Para probar el rendimiento de firmar URLs con diferentes módulos, reduje el método _sign_string para eliminar cualquier lógica excepto la firma de una cadena y luego creé una nueva clase de Distribution . Luego tomé la clave privada y la URL de ejemplo de las pruebas de boto para probar.

Los resultados muestran que la criptografía es más rápida, pero aún requiere cerca de 1 ms por solicitud de firma. Estos resultados están sesgados por el uso de iPython de las variables de ámbito en el tiempo.

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

El guion completo:

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"

Conclusión

Aunque el módulo de criptografía funciona mejor en esta prueba, recomiendo tratar de encontrar una forma de utilizar las cookies firmadas, pero espero que esta información sea útil.

Un caso de uso común de AWS S3 y CloudFront sirve contenido privado. La solución común es utilizar URL firmadas de CloudFront para acceder a archivos privados almacenados con S3.

Sin embargo, la generación de estas URL tiene un costo: calcular la firma RSA de cualquier URL determinada con una clave privada. Para Python (o boto , el SDK de Python de AWS), la biblioteca rsa ( https://pypi.python.org/pypi/rsa ) se usa para esta tarea. En mi MBP de finales de 2014, se necesitan aproximadamente ~ 25 ms por computación con una clave de 2048 bits.

Este costo puede afectar la escalabilidad de una aplicación que utiliza este enfoque para autorizar el acceso a contenido privado a través de CloudFront. Imagine que varios clientes solicitan acceso a múltiples archivos frecuentemente a 25 ~ 30ms / req.

Me parece que no se puede mejorar mucho en el cálculo de la firma en sí, aunque la biblioteca de rsa mencionada anteriormente se actualizó por última vez hace casi 1,5 años. Me pregunto si hay otras técnicas o diseños que pueden optimizar el rendimiento de este proceso para lograr una mayor escalabilidad. ¿O simplemente tenemos que agregar más hardware e intentar resolverlo de una manera de fuerza bruta?

Una optimización puede hacer que el punto final API acepte múltiples firmas de archivos por solicitud y devuelva las URL firmadas a granel en lugar de tratarlas individualmente en solicitudes separadas, pero el tiempo total necesario para calcular todas esas firmas todavía está allí.


Brevemente

Considere si puede (además de usar python-cryptography , per @ erik-e) usar una longitud de clave más corta (y probablemente cambie las claves con más frecuencia ), dados los detalles de su caso de uso. Si bien puedo firmar con la AWS de 2048 bits generada en ~ 1550 μs, solo toma ~ 307 μs a 1028 bits, ~ 184 μs a 768 bits y ~ 113 μs a 512 bits.

Explicación

Después de investigar esto un poco, voy a ir en otra dirección y construir a partir de la (ya gran) respuesta @ erik-e dio. Debo mencionar antes de entrar en eso que no sé cuán aceptable es esta idea; Acabo de informar sobre el impacto en el rendimiento que tiene (ver al final de la publicación una pregunta que hice en el SE de seguridad que solicita información sobre esto).

Estaba recopilando sincronizaciones para firmar con cryptography como sugiere @ erik-e, y debido al gran abismo de rendimiento existente entre él y nuestro método de firma existente para S3, decidí perfilar el código para ver si parecía que había algo obvio. masticando tiempo:

>>> 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}

Si bien puede haber algunos ahorros pequeños al acecho dentro del signer , la gran mayoría del tiempo se gasta dentro de la llamada a finalize (), y casi todo ese tiempo se gasta dentro de la llamada de firma real a openssl. Si bien esto fue un poco decepcionante, fue un claro indicador de que debería ver el proceso de firma para ahorrar.

Solo estaba usando la clave de 2048 bits CloudFront que se generó para nosotros, así que decidí ver qué impacto tendría una clave más pequeña en el rendimiento. Volví a ejecutar el perfil con la tecla más corta:

>>> 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}

Como mencioné en mi comentario sobre la respuesta de erik-e, el tiempo de ejecución que vi para nuestro método de firma completo usando la clave de 2048 bits con el módulo de cryptography fue ~ 1550 μs. La repetición de esta misma prueba con la clave de 512 bits reduce el tiempo de ejecución a aproximadamente ~ 113μs (un tiro de piedra de los ~ 30μs de nuestro método de firma S3).

Este resultado parece significativo, pero depende de cuán aceptable es usar una clave más corta para su propósito . Pude encontrar un comentario de marzo sobre un informe de problema de Mozilla que sugería que una clave de 512 bits podría romperse por $ 75 en 8 horas en EC2 .