13

Powszechnym zastosowaniem AWS S3 i CloudFront jest obsługa prywatnych treści. Najczęstszym rozwiązaniem jest używanie podpisanych adresów CloudFront do uzyskiwania dostępu do prywatnych plików przechowywanych za pomocą S3.Jak radzić sobie z wydajnością generowania podpisanych adresów URL, aby uzyskać dostęp do prywatnych treści za pośrednictwem CloudFront?

Jednak generowanie tych adresów URL wiąże się z kosztem: obliczanie podpisu RSA pod dowolnym adresem URL za pomocą klucza prywatnego. W przypadku Pythona (lub boto, Python SDK AWS) do tego zadania jest używana biblioteka rsa (https://pypi.python.org/pypi/rsa). W moim późnym 2014 MBP, zajmuje około 25ms na każde obliczenie z kluczem 2048-bitowym.

Koszt ten potencjalnie wpływa na skalowalność aplikacji stosującej to podejście do autoryzowania dostępu do prywatnych treści za pośrednictwem CloudFront. Wyobraź sobie wielokrotne żądanie klientów o dostęp do wielu plików często w 25 ~ 30ms/req.

Wydaje mi się, że niewiele można poprawić na samym obliczaniu podpisu, chociaż wspomniana wyżej biblioteka rsa została ostatnio zaktualizowana prawie 1,5 roku temu. Zastanawiam się, czy istnieją inne techniki lub projekty, które mogą zoptymalizować wydajność tego procesu, aby osiągnąć wyższą skalowalność. Czy możemy po prostu wrzucić więcej sprzętu i spróbować rozwiązać go w brutalny sposób?

Jedna z optymalizacji może sprawić, że punkt końcowy interfejsu API będzie akceptował wiele podpisów plików na żądanie i zbiorczo odsyła podpisane adresy URL, zamiast zajmować się nimi oddzielnie w oddzielnych żądaniach, ale całkowity czas potrzebny na obliczenie wszystkich tych podpisów nadal istnieje.

+0

Myśląc o tym dzisiaj, osobiście. Czy mierzysz dosłownie tylko generowanie podpisu, czy zadania takie jak ładowanie klucza, przygotowywanie zasad itp.? – abathur

+0

@abathur, Zmierzyłem zarówno czas generowania podpisu (RSA) (klucz jest już buforowany po stronie serwera, a polityka jest już skonfigurowana), jak i czas dla obsługi całego żądania (w tym niektóre niestandardowe logiki). Tak więc na moim komputerze było to ~ 25ms do generowania sygnatur i ~ 30ms do przetworzenia całego żądania. – MLister

+0

@MLister: czy możesz podać fragment kodu pokazujący linie, których używasz do obliczania podpisu RSA przykładowego adresu URL? – Peque

Odpowiedz

7

Stosować Signed Cookies

Gdy używam CloudFront z wielu prywatnych adresów URL, wolę używać Signed Cookies gdy wszystkie restrictions są spełnione. Nie przyspiesza to generowania podpisanych plików cookie, ale zmniejsza liczbę żądań podpisywania, które mają wynosić jeden na użytkownika, dopóki nie wygasną.

Tuning RSA Generation Podpis

mogę sobie wyobrazić, może masz wymagania, które czynią podpisanych ciasteczek jako niepoprawną opcję. W tym przypadku próbowałem przyspieszyć proces podpisywania, porównując moduł RSA używany z boto i cryptography. Dwie dodatkowe opcje to: m2crypto i pycrypto, ale do tego przykładu użyję kryptografii.

Aby przetestować wydajność podpisywania adresów URL za pomocą różnych modułów, zmniejszyłem metodę _sign_string w celu usunięcia logiki z wyjątkiem podpisania ciągu znaków, a następnie utworzenia nowej klasy Distribution. Następnie wziąłem klucz prywatny i przykładowy adres URL z boto tests, aby przetestować.

Wyniki pokazują, że kryptografia jest szybsza, ale nadal wymaga blisko 1 ms na każde żądanie podpisania. Wyniki te są przekrzywione na wyższym poziomie przez użycie przez iPythona zmiennych o zakresie w czasie.

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 

Pełen skrypt:

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" 

Wnioski

Chociaż moduł kryptograficzny działa lepiej w tym teście, polecam próbuje znaleźć sposób, aby wykorzystać podpisane ciasteczka, ale mam nadzieję, że ten informacje są przydatne.

+0

Dzięki. Szukałem opcji i nie miałem wystarczająco dużo czasu ostatniej nocy, aby przetestować wersję "kryptografii". Pomyślałem, że dodaję niektóre z moich danych czasowych na wypadek, gdyby pomogły komuś później: ~ 1550μs (1,5ms) dla metody podpisywania 'kryptografii', ~ 37ms dla metody podpisywania' rsa' (obydwa w porównaniu z ~ 30μs dla istniejącego s3 metoda podpisywania.) – abathur

+0

@ erik-e dzięki za odpowiedź. Spojrzałem trochę na podpisane ciasteczka, ale wygląda na to, że 'boto' nie obsługuje go obecnie. Czy zwinąłeś swój własny? – MLister

+0

Tak, ale użyłem tej samej metody '_sign_string' z' boto'. Ponieważ czas nie był tak ważny, nie zmieniłem domyślnej implementacji 'rsa' w' boto'. –

4

Krótko

Zastanów się, czy można (oprócz korzystania python-cryptography Per @ erik-E) używać krótszej długości klucza (i prawdopodobnie change keys more frequently), biorąc pod uwagę dane dotyczące Twojej przypadku użycia. Chociaż mogę podpisać 2048-bitowy klucz AWS wygenerowany w ~ 1550μs, zajmuje to tylko 307μs przy 1028 bitach, ~ 184μs przy 768 bitach i ~ 113μs przy 512 bitach.

Wyjaśnienie

Po patrząc na to przez chwilę, mam zamiar iść w innym kierunku i budować off z (już wielki) Odpowiedź @ erik-e dał. Powinienem wspomnieć, zanim do tego dojdę, że nie wiem, jaki jest ten pomysł; Właśnie relacjonuję wpływ na wydajność (patrz koniec postu na pytanie, które zadałem w sprawie bezpieczeństwa SE, szukając informacji na ten temat).

Zbierałem czasy na podpisywanie z cryptography, jak sugeruje @ erik-e, i ze względu na wciąż dużą przepaść w wydajności między nim a naszą obecną metodą podpisywania dla S3, postanowiłem profilować kod, aby zobaczyć, czy wyglądało jak może być cokolwiek oczywiste żucia czas:

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

Choć nie mogą być pewne niewielkie oszczędności czai wewnątrz signer, większość czasu spędza wewnątrz finalizuje() połączenia, a prawie cały ten czas spędzony wewnątrz rzeczywistego połączenia z signsl do openssl. Chociaż było to trochę rozczarowujące, było to wyraźnym wskaźnikiem, że powinienem patrzeć na faktyczny proces podpisywania oszczędności.

Właśnie wykorzystałem wygenerowany dla nas 2048-bitowy klucz CloudFront, więc zdecydowałem się zobaczyć, jaki wpływ na wydajność miałby mniejszy klucz. I ponownie prowadził profil za pomocą krótszego klucza:

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

Jak wspomniano w moim komentarzu odpowiedź Erik-E, w runtime widziałem dla naszej pełnej metody podpisywania przy użyciu klucza 2048-bitowego z modułem cryptography było ~ 1550μs. Powtórzenie tego samego testu za pomocą 512-bitowego klucza powoduje obniżenie czasu działania do około 113μs (rzut kamieniem od ~ 30μs naszej metody podpisywania S3).

Ten wynik wydaje się znaczący, ale zależy od how acceptable it is to use a shorter key for your purpose. W marcu udało mi się znaleźć komentarz do raportu o problemach Mozilli suggesting a 512-bit key could be broken for $75 in 8 hours on EC2.

+0

Czy wiesz, czy moduły modułu "kryptografii" mają 2 lub [3 czynniki pierwsze] (http://cacr.uwaterloo.ca/techreports/2006/cacr2006-16.pdf)? –

+0

@RickyDemer Nie będę udawać, że wiem; to, co dotychczas przeczytałem o ich kodzie, sugeruje, że odpowiedź brzmi: cokolwiek używa openssl. Jeśli * jestem * podążam, myślę, że to oznacza, że ​​odpowiedź jest 2. – abathur

+0

Wow, doskonały podział. –

Powiązane problemy