Ostatnio napotkałem na problem z certyfikatami samodzielnie podpisanymi, a dokładniej z pobieraniem certyfikatu klienta przy jego połączeniu z serwerem. Taki prosty przykład (zaczerpnięty z dokumentacji AnyIO - strona serwera:
|
|
Aby mieć dostęp do certyfikatu klienta łączącego się z serwerem, musimy ustawić context.verify_mode na ssl.CERT_OPTIONAL lub ssl.CERT_REQUIRED
(wtedy też TLSAttribute.peer_certificate / TLSAttribute.peer_certificate_binary da nam w wyniku certyfikat klienta).
Ale ssl.CERT_OPTIONAL / ssl.CERT_REQUIRED wymuszają od razu sprawdzenie poprawności tego certyfikatu. Jeśli jest on podpisany przez znane dla systemu CA,
to nie mamy problemu. Schody zaczynają się, gdy mamy self signed certificate - czyli certyfikat samodzielnie podpisany. Nie możemy go pobrać w trakcie
do_handshake bez jego sprawdzania. A to sprawdzanie, z racji jego self signed da nam
wyjątek:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1131)
A bez przejścia do_handshake funkcja
SSLSocket.getpeercert() (z której wewnętrznie korzystają TLSAttribute.peer_certificate
i TLSAttribute.peer_certificate_binary) zwróci nam wyjątek ValueError).
Po przeglądzie dokumentacji biblioteki ssl okazało się, że nie ma tam mechanizmu, który w prosty sposób pozwalałby na pobranie certyfikatu klienta za pierwszym razem bez jego weryfikacji, a sprawdzanie go przy następnych połączeniach (po dodaniu go jako zaufanego dzięki funkcji SSLContext.load_verify_locations).
Znalazłem jednak pewne rozwiązanie tego problemu. Biblioteka ssl posiada nieudokumentowaną właściwość
SSLContext._msg_callback (fragment z kodu źródłowego ssl.py):
|
|
Callback ten wywoływany jest w trakcie interesującego nas procesu handshake z następującymi wartościami content_type i msg_type:
|
|
Wynik:
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.HANDSHAKE: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.CLIENT_HELLO: 1>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.HANDSHAKE: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.SERVER_HELLO: 2>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.CHANGE_CIPHER_SPEC: 20>
content_type=<_TLSContentType.CHANGE_CIPHER_SPEC: 20> msg_type=<_TLSMessageType.CHANGE_CIPHER_SPEC: 257>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.ENCRYPTED_EXTENSIONS: 8>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.CERTIFICATE_REQUEST: 13>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.CERTIFICATE: 11>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.CERTIFICATE_VERIFY: 15>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.FINISHED: 20>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.CHANGE_CIPHER_SPEC: 20>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_STATUS: 22>
content_type=<_TLSContentType.HANDSHAKE: 22> msg_type=<_TLSMessageType.CERTIFICATE: 11>
content_type=<_TLSContentType.HEADER: 256> msg_type=<_TLSContentType.APPLICATION_DATA: 23>
content_type=<_TLSContentType.INNER_CONTENT_TYPE: 257> msg_type=<_TLSMessageType.CERTIFICATE_URL: 21>
content_type=<_TLSContentType.ALERT: 21> msg_type=<_TLSAlertType.UNKNOWN_CA: 48>
Interesuje nas drugie wystąpienie content_type ==_TLSContentType.HANDSHAKE i msg_type == _TLSMessageType.CERTIFICATE. Wtedy przesyłany jest
certyfikat klienta, którego dane będą zawarte w data. Ale poprzedza je jeszcze nagłówek części pakietu, którego struktura (po uproszczeniu) wygląda
następująco:
|
|
msg_type zawsze powinno mieć wartość 11 (pakiet CERTIFICATE). Trzy następne pola to wielkości danych
- całego pakietu (
length) - części z certyfikatami (
certificates_len- w zależności czy mamyTLSv1_2, czyTLSv1_3- to pole ma różną długość!) - pierwszego certyfikatu (
first_cert_len).
Po tym wstępie (10 lub 11 bajtów) mamy właściwe dane binarne certyfikatu. Całość przetwarzamy za pomocą
struct:
|
|
Zawartość cert_DER możemy na tym etapie wczytać funkcją
SSLContext.load_verify_locations do kontekstu. Będzie to skutkowało…
uznaniem tego certyfikatu klienta za prawidłowo podpisany. Dane te możemy też konwertować do tekstowego formatu PEM za pomocą funkcji
DER_cert_to_PEM_cert i zapisać je na dysku..