W części pierwszej tworzyliśmy główny certyfikat CA przy użyciu Python.
Mając certyfikat urzędu możemy w sposób zautomatyzowany - również przy pomocy Python - tworzyć i podpisywać nim certyfikaty dla usług. Skrypt będzie wyglądał podobnie, natomiast wystawcą certyfikatu (Issuer) będzie nasz urząd, którego wcześniej wytworzony certyfikat załadujemy w programie.
Po co?
W świecie programistycznym istnieje słuszne powiedzenie: "Don't roll your own crypto". Kryptografia to bardzo złożona dziedzina, a tworzenie własnych kryptograficznych mechanizmów bywa bardzo niebezpieczne.
Niemniej, w dzisiejszym świecie procesy deweloperskie są bardzo złożone, infrastruktury mocno rozbudowane i rozproszone.
Środowiska produkcyjne są zazwyczaj zabezpieczone, co wiąże się też z wymogami prawnymi (np. GDPR). Często chcemy jednak, aby nasze środowiska deweloperskie i testowe były zbliżone do środowisk produkcyjnych, abyśmy mieli takie same konfiguracje bezpieczeństwa w każdym z tych wariantów. Skoro na produkcji szyfrujemy dane, zabezpieczamy ich transmisję, to być może chcielibyśmy mieć te same mechanizmy obecne podczas procesów deweloperskich. Skoro w środowisku produkcyjnym wymagamy mechanizmów bezpieczeństwa zawsze, to powinniśmy również je testować.
Przykładowo - jeśli w środowisku produkcyjnym szyfrujemy dane, to nasze testy powinny te wymagania uwzględniać. Jeśli tam wymagamy zawsze transmisji szyfrowanej, to powinniśmy i to testować. Środowiska produkcyjne cechują się jednak najczęściej większym stopniem "stałości" w zakresie platform i narzędzi, natomiast w środowiskach deweloperskich i testowych bywa znacznie więcej różnorakich rozwiązań oraz indywidualnych preferencji deweloperskich (systemy operacyjne, czy narzędzia programistyczne). Do tego dochodzi często spory narzut operacyjny związany z przygotowaniem środowisk testowych (choćby kontenery). Jeśli nie chcemy (lub nie możemy) używać stałych certyfikatów w danym środowisku, to możemy w miarę łatwo zautomatyzować ich generowanie (nawet "w locie").
O ile w środowisku produkcyjnym wymagamy wyższego poziomu zabezpieczeń i mocnych algorytmów kryptograficznych, o tyle w przypadku środowisk testowych nie zawsze skupiamy się już na samych algorytmach kryptograficznych, a sprawdzamy czy zabezpieczenia są obecne i czy konfiguracje spełniają nasze wymagania. Myślę, że w takim przypadku trochę wiedzy o certyfikatach SSL może się przydać, a automatyzacja ich generowania może oszczędzić nam nakładu pracy związanego z różnymi konfiguracjami.
W tym artykule zajmujemy się po prostu generowaniem certyfikatów, nie tworzymy własnych algorytmów kryptografinyczh. Uważać jednak należy i z tym, gdyż udokumentowane standardy są obszerne i skomplikowane, a błędne użycie rozwiązań może dawać złudne poczucie bezpieczeństwa.
Przed przystąpieniem do dalszej treści przypomnę zatem ostrzeżenie: "Don't roll your own crypto".
Tworzenie i podpis certyfikatu usługi
Skrypt w całości z komentarzami:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|
Numer seryjny jest liczbą.
Jeśli chcemy by nasz klucz prywatny nie był chroniony hasłem, to do funkcji dump_privatekey() nie przekazujemy dwóch ostatnich argumentów (cipher, passhprase).
Dane certyfikatu i klucza prywatnego możemy oczywiście zapisać do osobnych plików.
Niżej zobaczymy użycie komend openssl, które pozwalają na ekstrakcję danych certyfikatu/klucza z pojedynczego pliku pem, czy pozbycie się hasła chroniącego klucz prywatny certyfikatu.
Wykonanie i sprawdzenie
Dla przejrzystości, identyfikator klucza mojego urzędu to:
$ openssl x509 -text -in ca.pem | grep "Subject Key Identifier" -A 1
X509v3 Subject Key Identifier:
47:AA:D2:93:9D:4C:2A:01:34:42:49:F7:A8:C6:9E:B4:A8:5E:84:F4
Generuję nowy certyfikat serwera używjąc powyższego skryptu.
Skrypt ładuje certyfikat urzędu (plik ca.pem) oraz dane jego klucza prywatnego, które są chronione hasłem.
Program zapyta więc o to hasło.
Skrypt nazwałem gensrv.py, zatem uruchamiam:
$ python gensrv.py
Enter PEM pass phrase:
W wyniku otrzymujemy plik server.pem.
$ ls -la server.pem
-rw-r--r-- 1 me me 3423 Dec 4 23:04 server.pem
Zanim przejdziemy dalej, zweryfikujmy po prostu czy wszystko do tej pory się zgadza:
$ openssl verify -CAfile ca.pem server.pem
server.pem: OK
Wygląda poprawnie. Oczywiście, jeśli nie podalibyśmy pliku urzędu, weryfikacja nie powiodłaby się, ponieważ system operacyjny nie zna naszego urzędu. Taki błąd wyglądałby następująco:
$ openssl verify server.pem
CN = MyServer, C = XX, ST = Nowhere, L = Nowhere, O = Development, OU = Development, emailAddress = server@localhost
error 20 at 0 depth lookup: unable to get local issuer certificate
error server.pem: verification failed
Błąd unable to get local issuer certificate oznacza właśnie, że wśród certyfikatów urzędów zainstalowanych (i takoż zaufanych) w systemie, nie występuje urząd, który podpisał ten certyfikat (nasz własny urząd). To wszystko się zgadza.
Wyświetlamy dane certyfikatu (tutaj w krótszej, nieco zredagowanej dla czytelności formie).
$ openssl x509 -text -in server.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 20211204230426 (0x1261c9a60d1a)
Signature Algorithm: sha512WithRSAEncryption
Issuer: CN = MyCommonName, C = XX, ST = MyState, L = MyLocation, O = MyOrganization, OU = MyUnitName, emailAddress = me@localhost
Validity
Not Before: Dec 4 22:04:26 2021 GMT
Not After : Dec 5 22:04:26 2021 GMT
Subject: CN = MyServer, C = XX, ST = Nowhere, L = Nowhere, O = Development, OU = Development, emailAddress = server@localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:cf:22:0e:bf:e8:5a:[...]
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
39:A9:E3:21:7A:08:47:0F:1D:B2:6C:DA:E2:63:1D:A8:F6:72:27:1C
X509v3 Authority Key Identifier:
keyid:47:AA:D2:93:9D:4C:2A:01:34:42:49:F7:A8:C6:9E:B4:A8:5E:84:F4
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage: critical
TLS Web Server Authentication
X509v3 Subject Alternative Name: critical
IP Address:127.0.0.1, DNS:dev.lan, DNS:*.dev.lan
Signature Algorithm: sha512WithRSAEncryption
6d:02:9e:da:96:e4:d8[...]
Widzimy teraz, że odcisk klucza (fingerprint) podmiotu certyfikatu jest faktycznie inny niż odcisk klucza urzędu:
X509v3 Subject Key Identifier:
39:A9:E3:21:7A:08:47:0F:1D:B2:6C:DA:E2:63:1D:A8:F6:72:27:1C
X509v3 Authority Key Identifier:
keyid:47:AA:D2:93:9D:4C:2A:01:34:42:49:F7:A8:C6:9E:B4:A8:5E:84:F4
Identyfikator klucza podmiotu to:
39:A9:E3:21:7A:08:47:0F:1D:B2:6C:DA:E2:63:1D:A8:F6:72:27:1C,
a identyfikator urzędu (jak wyżej podałem), to:
47:AA:D2:93:9D:4C:2A:01:34:42:49:F7:A8:C6:9E:B4:A8:5E:84:F4.
Również same dane identyfikacyjne podmiotu certyfikatu są rózne od danych identyfikujących urząd.
Podmiot certyfikowany - server (Subject):
Subject: CN = MyServer, C = XX, ST = Nowhere, L = Nowhere, O = Development, OU = Development, emailAddress = server@localhost
Urząd (Issuer):
Issuer: CN = MyCommonName, C = XX, ST = MyState, L = MyLocation, O = MyOrganization, OU = MyUnitName, emailAddress = me@localhost
Używamy certyfikatu w usłudze (minimalna aplikacja webowa w Python)
Skoro mamy certyfikat dla usługi, to spróbujmy go użyć. Moglibyśmy się posłużyć jakimś gotowym programem serwerowym, jednak dla minimalizacji przykładu użyję najprostszej aplikacji webowej w FastAPI. Aplikację uruchomię za pomocą uvicorn, któremu przekażę lokalizację pliku z certyfikatem i kluczem serwera.
Najpierw instaluję FastApi oraz uvicorn:
pip install fastapi uvicorn
Kod aplikacji webowej:
import fastapi
app = fastapi.FastAPI()
@app.get("/")
async def hello():
return "Hello"
Uruchomienie (HTTPS):
$ uvicorn myapp:app --ssl-certfile=server.pem --ssl-keyfile server.pem --port 8443
Enter PEM pass phrase:
INFO: Started server process [2266279]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on https://127.0.0.1:8443 (Press CTRL+C to quit)
Jak widać powyżej - nasz klucz prywatny serwera jest wciąż zabezpieczony hasłem, dlatego nie da się wystartować usługi bez podania hasła. Jeśli chcielibyśmy usunąć hasło i pozostawić klucz prywatny bez tego zabezpieczenia (co pozwoli uruchomić aplikację bez podawania hasła), to możemy osiągnąć to używając "openssl rsa". Jako plik wejściowy podaję server.pem, a jako wynikowy - plik server.key, w którym znajdzie się niezabezpieczony klucz prywatny.
$ openssl rsa -in server.pem -out server.key
Enter pass phrase for server.pem:
writing RSA key
Teraz wydając podobną komendę uvicorn (różnica: --ssl-keyfile server.key) mogę uruchomić aplikację bez podania hasła.
$ uvicorn myapp:app --ssl-certfile=server.pem --ssl-keyfile server.key --port 8443
INFO: Started server process [2270531]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on https://127.0.0.1:8443 (Press CTRL+C to quit)
Testujemy połączenia i weryfikację certyfikatów
Nasza przykładowa aplikacja została uruchomiona, usługa nasłuchuje na porcie 8443. Jest to aplikacja webowa działająca na protokole HTTPS, a zatem użyję najpierw serii komend konsolowych do komunikacji z nią, a później sprawdzę komunikację z poziomu przeglądarek internetowych.
openssl s_client
s_client to moduł openssl, który pozwala nam nawiązać szyfrowane połączenie na wskazany adres/port.
Posiada również opcję -showcerts, która wyświetli nam informacje o certyfikacie, jakim przedstawia się serwer.
$ openssl s_client -connect 127.0.0.1:8443 -showcerts -CAfile ca.pem
curl (połączenie HTTPS i weryfikacja)
Teraz sprawdzę połączenie przy użyciu curl. Certyfikat urzędu jest self-signed, nie jest zainstalowany w systemie operacyjnym, a zatem curl powinien poinformować o błędzie przy weryfikacji certyfikatu. Sprawdźmy czy tak się dzieje.
$ curl https://127.0.0.1:8443
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
To się zgadza. Możemy zatem przekazać do curl argument określający ścieżkę pliku z certyfikatem urzędu.
Jeśli nasza aplikacja serwerowa przedstawia się certyfikatem podpisanym przez ten urząd, połączenie powinno się odbyć bez błędu.
Możemy do weryfikacji użyć bezpośrednio pliku ca.pem (certyfikat i klucz), ale zwyczajowo mamy do dyspozycji jedynie certyfikat.
Dla jasności wyeksportujmy więc certyfikat urzędu z pliku ca.pem do pliku ca.crt:
$ openssl x509 -in ca.pem -out ca.crt
A teraz sprawdzamy połączenie curl ze wskazaniem certyfikatu naszego urzędu:
$ curl https://127.0.0.1:8443 --cacert ca.crt
"Hello"
Jak widać nasza aplikacja webowa odpowiedziała "Hello", a połączenie tym razem odbyło się bez błędu,
gdyż program curl sprawdził czy certyfikat aplikacji pochodz faktycznie z naszego urzędu.
Sprawdzamy przeglądarki internetowe
Firefox
Otwieramy stronę aplikacji w nowym oknie w trybie prywatnym:
$ firefox --private-window https://127.0.0.1:8443
Firefox prawidłowo wyświetla monit bezpieczeństwa:
Sprawdzamy dane certyfikatu w Firefox:
- dane podmiotu certyfikowanego (Subject)
- dane urzędu (Issuer)
- okres ważności certyfikatu
Patrzymy zatem dalej:
- Subject Alt Names (ikonka wykrzyknika - oznaczenie critical)
- informacje o kluczu publicznym (algorytm, liczba bitów)
- przy podpisywaniu użyliśmy algorytmu SHA-512
- wersja 3
- odciski (identyfikatory klucza /algortymy SHA-256, SHA-1/)
Sprawdzamy rozszerzenia:
+ certyfikat nie jest certyfikatem urzędowym
+ KeyUsage wskazuje na ustawione przez nas "Digital Signature" oraz "Key Encipherment" (ikonka critical)
+ ExtendedUsage to "Server Authentication" (również critical)
+ identyfikator klucza tego certyfikatu: 39:A9:E3:21:7A:08:47:0F:1D:B2:6C:DA:E2:63:1D:A8:F6:72:27:1C
+ identyfikator klucza urzędu: 47:AA:D2:93:9D:4C:2A:01:34:42:49:F7:A8:C6:9E:B4:A8:5E:84:F4
Wracamy do głównej strony monitu i wyświetlamy jego szczegóły:
Błąd informuje nas: "certificate issuer is uknown", co się zgadza (niezaufany self-signed certificate, niezainstalowany w systemie).
Akceptuję ryzyko i przechodzę do strony aplikacji.
"Hello".
Chromium
Poziomy sprawdzania certyfikatów przez przeglądarki bywają różne.
Przykładowo chromium odmawia walidacji certyfikatów, które nie zawierają subjectAltName (czyli nie mają wskazanych adresów ip/nazw domenowych).
Dlatego dla pewności podobne sprawdzenie wykonuję w przeglądarce chromium.
$ chromium --incognito https://127.0.0.1:8443
Ostrzeżenie faktycznie wskazuje na tę samą przyczynę - certyfikat urzędu nie jest zaufany w moim
systemie operacyjnym.
Sprawdzam dane certyfikatu:
Akceptuję ryzyko i przechodzę do aplikacji:
"Hello".