Python/sockets/ssl EOF произошел с нарушением протокола

Я хотел бы аутентифицировать сервер на стороне клиента в моей программе echo client/server. Я использую python 2.7.12 и модуль ssl на

Distributor ID: Ubuntu
Description:    Ubuntu 14.04.5 LTS
Release:        14.04
Codename:       trusty

Я создал сертификаты и ключи клиентов и серверов с помощью команд openssl:

openssl req -new -x509 -days 365 -nodes -out client.pem -keyout client.key
openssl req -new -x509 -days 365 -nodes -out server.pem -keyout server.key

Версии библиотеки openssl и openssl, используемые python, одинаковы:

openssl version -a
OpenSSL 1.0.1f 6 Jan 2014
built on: Fri Sep 23 12:19:57 UTC 2016
platform: debian-amd64
options:  bn(64,64) rc4(16x,int) des(idx,cisc,16,int) blowfish(idx) 
compiler: cc -fPIC -DOPENSSL_PIC -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -m64 -DL_ENDIAN -DTERMIO -g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -D_FORTIFY_SOURCE=2 -Wl,-Bsymbolic-functions -Wl,-z,relro -Wa,--noexecstack -Wall -DMD32_REG_T=int -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM
OPENSSLDIR: "/usr/lib/ssl"

python -c "import ssl; print ssl.OPENSSL_VERSION"
OpenSSL 1.0.1f 6 Jan 2014

Однако приведенный ниже код показывает некоторые ошибки на стороне сервера: EOF occurred in violation of protocol (_ssl.c:1645) (но сервер все еще работает) и на стороне клиента:

Traceback (most recent call last):
  File "/http_ssl_client.py", line 36, in <module>
    if not cert or ('commonName', 'test') not in cert['subject'][4]: raise Exception("Invalid SSL cert for host %s. Check if this is a man-in-themiddle attack!" )
Exception: Invalid SSL cert for host %s. Check if this is a man-in-themiddle attack!
{'notBefore': u'Jun  3 11:54:21 2017 GMT', 'serialNumber': u'BBDCBEED69655B6E', 'notAfter': 'Jun  3 11:54:21 2018 GMT', 'version': 3L, 'subject': ((('countryName', u'pl'),), (('stateOrProvinceName', u'test'),), (('localityName', u'test'),), (('organizationName', u'test'),), (('organizationalUnitName', u'test'),), (('commonName', u'test'),), (('emailAddress', u'test'),)), 'issuer': ((('countryName', u'pl'),), (('stateOrProvinceName', u'test'),), (('localityName', u'test'),), (('organizationName', u'test'),), (('organizationalUnitName', u'test'),), (('commonName', u'test'),), (('emailAddress', u'test'),))}

Код сервера:

#!/bin/usr/env python
import socket
import ssl

def main():
    HOST = '127.0.0.1'
    PORT = 1234

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((HOST, PORT))
    sock.listen(5)

    while True:
        conn = None
        client_sock, addr = sock.accept()
        try:
            ssl_client = ssl.wrap_socket(client_sock, server_side=True, certfile="server.pem", keyfile="server.key", ssl_version=ssl.PROTOCOL_TLSv1_2)
            data =  ssl_client.read(1024)
            print data
            ssl_client.write(data)
        except ssl.SSLError as e:
            print(e)
        finally:
            if conn:
                conn.close()
if __name__ == '__main__':
    main()

Клиент:

#!/bin/usr/env python
import socket
import ssl

if __name__ == '__main__':

    HOST = '127.0.0.1'
    PORT = 1234

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((HOST, PORT))

    context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
    context.verify_mode = ssl.CERT_REQUIRED
    context.load_verify_locations('server.pem')

    if ssl.HAS_SNI:
        secure_sock = context.wrap_socket(sock, server_hostname=HOST)
    else:
        secure_sock = context.wrap_socket(sock)

    cert = secure_sock.getpeercert()
    print cert

    if not cert or ('commonName', 'test') not in cert['subject'][4]: raise Exception("Error" )

    secure_sock.write('hello')

    print secure_sock.read(1024)

    secure_sock.close()
    sock.close()

Все файлы находятся в одном каталоге.

Ответ 1

Я собираюсь пройтись по проблемам, которые я обнаружил (хотя некоторые уже были замечены кем-то другим, а также исправлены). Обратите внимание, что я пробовал на Win 10, используя Python 2.7.10, 2.7.13, 3.4.4, 3.5.3 и OpenSSL 1.0.2d, 1.0.2j (без ф/ф):

  • server.py:

    • переменная conn инициализируется как None и что это. Предложение if в конце finally бесполезно. client_sock, вероятно, должен быть проверен
    • Подсказка: серверный сокет (sock), вероятно, может быть упакован (вместо client_sock); таким образом client_sock (возвращаемый sock.accept()) уже будет упакован
  • client.py:

    • 1- й является второстепенным: код во фрагменте (raise Exception("Error")) не совпадает с кодом в трассировке (raise Exception("Invalid SSL cert for host %s. Check if this is a man-in-themiddle attack!"))
    • Способ поиска атрибутов сертификата. Как заметил @TomaszPlaskota, индекс кортежа commonName неверен. Вот довольно распечатанный сертификат с моей машины (значения полей могут/будут отличаться):

      {
          'issuer': ((('countryName', 'AU'),),
                     (('stateOrProvinceName', 'Some-State'),),
                     (('localityName', 'CJ'),),
                     (('organizationName', 'Internet Widgits Pty Ltd'),),
                     (('organizationalUnitName', 'OU'),),
                     (('commonName', 'cfati-e5550-0'),),
                     (('emailAddress', '[email protected]'),)),
          'notAfter': 'Jun  5 17:21:03 2018 GMT',
          'notBefore': 'Jun  5 17:21:03 2017 GMT',
          'serialNumber': 'C4A03B2BE4F959A9',
          'subject': ((('countryName', 'AU'),),
                      (('stateOrProvinceName', 'Some-State'),),
                      (('localityName', 'CJ'),),
                      (('organizationName', 'Internet Widgits Pty Ltd'),),
                      (('organizationalUnitName', 'OU'),),
                      (('commonName', 'cfati-e5550-0'),),
                      (('emailAddress', '[email protected]'),)),
          'version': 3
      }
      

      Могут быть случаи (по крайней мере теоретически), когда сертификат будет неполным. Более надежная форма проверки:

      def check_certificate(cert, field=("commonName", "test")):
          if not cert:
              return False
          for pairs in cert.get("subject", ()):
              if field in pairs:
                  return True
          return False
      
      # ....
      
      if not check_certificate(cert):
          raise Exception("Error")
      
    • Способ обработки ошибок. Если сертификат сервера не в порядке, вы просто выдаете ошибку, которая нарушает связь и ненормально завершает работу клиентской программы. Тот факт, что на стороне сервера триггеры ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:590) (из вашего другого вопроса: [SO]: взаимная аутентификация ssl в простом ECHO клиент/сервер [Python/sockets/ssl] modules], ssl.SSLEOFError: EOF произошла с нарушением протокола), когда сервер пытается выполнить обратную запись для клиента (ssl_client.write(data)).
      Хотя в некоторых случаях при завершении программы ОС очищает ресурсы, рекомендуется всегда выполнять очистку из кода. Итак, вместо того, чтобы просто вызвать исключение, сделайте что-то вроде (как предложено @JamesKPolk):

      plain_sock = secure_sock.unwrap()
      plain_sock.shutdown(socket.SHUT_RDWR)
      plain_sock.close()
      # Any other cleanup action here
      
  • Самая большая проблема заключается в том, что вы используете 2 самозаверяющих сертификата (которые не имеют ничего друг к другу). Самоподписанный сертификат означает:

    • Это собственный подписант (или родитель). Вот почему поля "Эмитент" и "Тема" совпадают (или, если быть более точными, расширения идентификатора ключа эмитента и идентификатора ключа субъекта совпадают)
    • Это сертификат CA (Certificate Authority) (если посмотреть на его базовые ограничения, вы заметите, что Subject Type is CA). Это как следствие бывшей пули

Несколько слов о сертификатах: они организованы в деревья: это означает, что будет корневой узел, также известный как сертификат корневого центра сертификации. Этот узел может иметь несколько дочерних узлов (дочерний узел означает, что он подписан своим родителем). Этими узлами также могут быть сертификаты CA или конечного пользователя (ЕС). У каждого сертификата CA также могут быть дети, и есть наше дерево. Его листья являются сертификатами ЕС. Путь между корневым ЦС и листовым сертификатом (составленным из корневого ЦС, промежуточных ЦС и ЕС) называется цепочкой сертификатов.
Самоподписанный сертификат может быть представлен в виде дерева, состоящего из одного узла. Обратите внимание, что, будучи сертификатом CA (как в нашем случае), он также может быть использован для подписи других сертификатов (он может иметь детей).

Проверка сертификата - когда сертификат проверяется, чтобы удостовериться, что это "кто" утверждает, что это так. Это делается путем проверки его по отношению к его родительскому ЦС (и рекурсивно все ЦС в цепочке проверяются до достижения корневого ЦС). Если все в порядке, то сертификат действителен. Это, конечно, очень упрощенная версия, тема довольно сложная, но в Интернете можно найти много информации. Само собой разумеется, что объект, который выполняет проверку, должен иметь доступ к CA в цепочке. Вы можете взять веб-браузер в качестве примера, его сертификат "хранилище" содержит:

  • Дополнительно: сертификаты (EU), которые были предоставлены вам для возможности подключения к некоторым сайтам (например, личные сертификаты)
  • Сертификаты (CA), необходимые для проверки других сертификатов, представленных веб-серверами (Trusted Root/Intermediate Certification Authorities)

[IBM]: обзор рукопожатия SSL или TLS (хотя есть много других мест) кратко описывает, как работает соединение SSL.
Как правило, защищенная система будет выдавать (уникальный) сертификат (ЕС) для каждого из своих клиентов; этот сертификат будет привязан к клиентскому компьютеру (* IP8-адрес или полное доменное имя); здесь CRL и OCSP стоит упомянуть.

Вернемся к вопросу: поскольку у нас есть особая ситуация (2 сертификата, где каждый является CA, но они также используются как ЕС), это может быть не так очевидно, но я сделаю все возможное, чтобы объяснить. Учитывая, что проверка сертификата будет происходить на обоих концах связи (двусторонняя), оба сертификата должны быть загружены в обоих приложениях. Например, в серверном приложении:

Очевидно, что для клиента все будет наоборот.