SSL pinning во Flutter

    Для большинства мобильных приложений рано или поздно приходит время роллаута. И если к этому моменту вы ещё не позаботились хотя бы о минимальной защите от атак, то чем ближе будет подходить время массового запуска, тем острее будет стоять данный вопрос. 

    Для мобильных приложений, работающих с сервером и конфиденциальными данными, однозначно стоит позаботиться о безопасности своих пользователей от довольно популярной атаки — MITM.



    MITM


    Давайте разберёмся, что же это за атака. Атака посредника, или атака «человек посередине» (англ. Man in the middle) — вид атаки в криптографии и компьютерной безопасности, когда злоумышленник тайно ретранслирует и при необходимости изменяет связь между двумя сторонами, которые считают, что они непосредственно общаются друг с другом. Является методом компрометации канала связи, при котором взломщик, подключившись к каналу между контрагентами, осуществляет вмешательство в протокол передачи, удаляя или искажая информацию.

    Изначально протокол Http является уязвимым для данного типа атаки и для исправления данной уязвимости была создана защищённая версия этого протокола Https. Он использует SSL или TLS для шифрования соединения между клиентом и сервером. Работают эти схемы следующим образом: в начале общения сервер посылает клиенту свой сертификат, чтобы клиент идентифицировал его. Данный сертификат стандарта X.509 является файлом, в котором содержится следующая информация:

    • Название
    • Публичный ключ
    • Серийный номер
    • Алгоритм сертификата
    • Цифровая подпись

     Идентификация проходит по нескольким пунктам:

    1. Имя сервера должно совпадать с указанным в сертификате, иначе сертификат можно считать скомпрометированным.
    2. Цепочку SSL сертификата можно проследить от личного SSL сертификата через промежуточные и до корневого сертификата доверенного центра сертификации.

    Рассмотрим второй пункт более подробно. 

    Поскольку установка каждого сертификата отдельно была бы просто невозможна, ведь их огромное количество, работает следующая система управления сертификатами. Так как сертификаты являются фактически удостоверениями серверов, то они не появляются сами  — их выпускают центры сертификации (Certificate Authority). Самыми важными являются сертификаты CA, их еще называют корневыми. CA являются общеизвестными, и их ключи являются доверенными по умолчанию. Они как правило встраиваются в операционную систему и обновляются при следующих обновлениях. Сертификаты работают по строгой иерархии. Сертификаты СА могут подписывать другие сертификаты, они в свою очередь подписывают сертификаты следующего уровня и так далее. В случае компрометации сертификата, он может быть отозван и вместе с ним автоматически отзываются все дочерние сертификаты.

    Выглядит довольно надёжно, но что если пользователь сам скомпрометирует хранилище сертификатов в своей системе, установив в доверенные сертификат злоумышленника.

    SSL-pinning


    SSL-pinning – привязка сертификата или публичного ключа сервера к клиенту. В случае с мобильным приложением, одним из эффективных способов является внедрение в приложение SSL сертификата, которому мы собираемся доверять. В данном случае мы игнорируем хранилище системы и сами определяем какому сертификату мы будем доверять. Этот способ может помочь нам при необходимости использовать самоподписанный сертификат, не утруждая конечного пользователя его установкой.

    SSL-pinning в Dart


    Http запросы в Dart осуществляются при помощи класса HttpClient, точнее при помощи его приватной стандартной реализации. Каким будет подключение обычным или защищённым определяется исходя из схемы адреса к которому мы подключаемся.

    bool isSecure = (uri.scheme == «https»);

    В случае использования защищённого соединения, будет использован SecureSocket, который попытается  установить защищённое соединение. При неудаче нас уведомят, что соединение использует скомпрометированный сертификат, если мы заранее зарегистрировали функцию обработки данной ситуации. 

    Регистрируется она с помощью поля badCertificateCallback у HttpClient.При помощи этой функции мы можем не только узнать, что сертификат скомпрометирован, но и обработать данный момент. Надо принять решение — продолжаем мы данный запрос или нет.

    Если мы не станем добавлять обработчик, соединение будет разорвано при провале стадии проверки сертификата.

    На что же опирается SecureSocket во время проверки сертификата? Стандартное поведение в данном случае — проверка с учётом тех сертификатов, которые зарегистрированы в системе. Как мы уже определили раньше, не самое лучшее решение для надёжной проверки. Нам необходима возможность проверки именно конкретного сертификата или цепочки сертификатов.

    В реализации dart за это отвечает класс SecurityContext. Исходя из его описания мы узнаём, что он является объектом, хранящим сертификаты, которым доверяют при создании безопасного клиентского соединения, а также цепочку сертификатов и закрытый ключ для обслуживания с безопасного сервера. Этот объект можно передать HttpClientпри создании, и в таком случае, клиент будет опираться в проверки именно на данные, хранящиеся в контексте, игнорируя доверяет или нет этим сертификатам система.

    SecurityContext может создаваться через различные конструкторы и в зависимости от этого будет иметь разное содержание. Например, дефолтный конструктор создаст абсолютно пустой контекст, без зарегистрированных ключей.

    Конструктор же SecurityContext.defaultContext создаст контекст, который содержит некоторые наборы доверенных сертификатов, в зависимости от системы. В случае Windows или Linux будут использованы сертифика хранилища Mozilla Firefox. В случае MacOS, iOS, Android будут зарегистрированы сертификаты, которым доверяет система.

    Помимо этого контекст имеет методы для регистраций приватного ключа, доверенного сертификата и цепочки сертификатов.

    Для регистрации доверенного сертификата предлагается использовать один из двух методов.

    void setTrustedCertificates(String file, {String password});

    Данный метод позволяет указать путь до сертификата.

    void setTrustedCertificatesBytes(List<int> certBytes, {String password});

    Этот же метод использует для регистрации содержание сертификата в байтовом представлении.

    Даже если не обращать внимание на предупреждение разработчиков:
    NB: This function calls [File.readAsBytesSync], and will block on file IO. Prefer using [setTrustedCertificatesBytes],

    мне кажется более предпочтительным использование setTrustedCertificatesBytes. Причина довольно проста: если какой-то «нехороший человек» захочет навредить нам, ему для этого потребуется гораздо больше ресурсов. Если мы положим файл сертификата в обычном виде в ресурсы, при декомпиляции он сможет легко найти его и подменить. Да, он не сможет самостоятельно подписать пересобранное приложение, но никто не вредит пользователю с таким же усердием, как сам пользователь. Поэтому факт невозможности переподписать приложение теми же ключами, может не стать решающим фактором.
    Что если мы зашьём байтовое представление ключа в виде кода в приложение?
    Flutter при сборке приложения в release режиме использует AOT компиляцию, поэтому мы создадим довольно много проблем тому, кто захочет просто подменить сертификат, вскрыв приложение.

    Практическая реализация SSL-pinning


    Во-первых, для установки доверенного соединения вам потребуется сам файл сертификата, который использует сервер. Если вы по какой-то причине не можете обратиться к системным администраторам, или нет времени ждать пока вам его предоставят, можете довольно легко получить его с помощью браузера. В данном примере я разберу вариант с Google Chrome, но с таким же успехом можете сделать это в большинстве других браузеров.

    Переходим по адресу, сертификат которого собираемся получить. Например www.facebook.com. Если соединение защищено, в адресной строке мы увидим изображение замка, при нажатии на который мы откроем информацию о соединении.

    image

    Нас интересует сертификат, переходим внутрь.

    image

    В открывшемся окне мы видим цепочку подписания сертификатов. Конечный сертификат и будет принадлежать данному ресурсу.

    Перетащив сертификат в любую директорию на своём компьютере вы сохраните файл сертификата.

    Другой способ получить тот же самый файл сертификата.

    image

    В инструментах разработчика переходим на вкладку Security. Нажатие на View Certificate открывает уже знакомое нам окно просмотра сертификата, откуда его мы сохраняем на компьютер.

    Данный файл сертификата будет иметь суффикс .cer, одно из представлений файла сертификата. Один и тот же сертификат может быть представлен в различных форматах и от этого зависит способ кодирования данных о сертификате и тд. Если мы прочитаем документацию к методам добавления сертификатов в доверенные в SecurityContext, то узнаем, что нам нужен определённый формат сертификата — PEM или PKCS12.
    Суффикс .cer для сертификата используется в нескольких форматах: PEM и DER. Один для нас подходящий, второй — нет. Чтобы определить, что перед нами находится, нужно открыть сертификат в текстовом редакторе и посмотреть на содержимое. Если видим, что текст кода в нём начинается с тега "----- BEGIN CERTIFICATE -----" и заканчивая тегом "----- END CERTIFICATE -----", то всё отлично. Если увидим, что просто открыли бинарный файл в текстовом виде, то перед нами DER.

    В случае если формат не подходит, придётся конвертировать. Для конвертации можно воспользоваться любым из сервисов, я рассмотрю один из способов.

    В командной строке на уровне, где находится файл сертификата выполняем команду:

    openssl x509 -inform der -in certificate.cer -out certificate.pem

    В которой certificate.cer имя сертификата, который конвертируется, а certificate.pem имя сертификата после конвертации.

    Итак, у нас есть сертификат в нужном нам формате. Нам нужно прочитать его в виде байтов и сохранить это значение, чтобы в дальнейшем использовать в приложении. Для хранения этих данных вполне подойдёт List. Для удобства я написал небольшой скрипт, который считывает сертификат в виде набора байт и создаёт Dart файл с переменной, хранящей данное значение.

    /// Util for byte list representation of certificate.
    ///
    /// You should set certificate name in args.
    /// Certificate should be in certs folder.
    ///
    /// Call example:
    /// dart main.dart cert.pem
    ///
    /// Exit codes:
    /// 0 - success
    /// 1 - error
    void main(List<String> arguments) {
      exitCode = 0;
      final parser = ArgParser();
    
      var args = parser.parse(arguments).arguments;
    
      if (args.length < 1) {
        exitCode = 1;
    
        throw Exception('You should set certificate name.');
      }
    
      var fileName = args[0];
    
      var certFile = File("certs/$fileName");
    
      if (!certFile.existsSync()) {
        exitCode = 1;
    
        throw Exception('File certificate ${certFile.path} not found.');
      }
    
      String fileNameWithoutExt = basenameWithoutExtension(certFile.path);
    
      var cert = certFile.readAsBytesSync();
      var res = "List<int> $fileNameWithoutExt = <int>[${cert.join(', ')}];";
      var resFile = File("res/$fileNameWithoutExt.dart");
    
      if (!resFile.existsSync()) {
        resFile.createSync(recursive: true);
      }
    
      resFile.writeAsString(res);
    }
    

    Теперь нам нужно лишь скопировать данный файл в проект. Использование в проекте будет довольно простым.

    SecurityContext context = SecurityContext();
      context.setTrustedCertificatesBytes(cert);
      var http = new HttpClient(context: context);
    
      http.badCertificateCallback =
          (X509Certificate cert, String host, int port) {
        print("!!!!Bad certificate");
        return false;
      };
    
      await http
          .openUrl("GET", Uri.parse(_url).normalizePath(),)
          .timeout(Duration(milliseconds: 3000));
    

    Если мы попытаемся обратиться по адресу, и сертификат предоставленный им при проверке будет не совпадать с тем, который мы зашили в приложение, будет выброшена ошибка HandshakeException.

    Заключение


    Надеюсь, эта статья будет полезна при написании приложений и поможет лучше защитить ваших пользователей от данного типа атак.

    Спасибо, за внимание!

    Используемые материалы: 
    Атака посредника
    Формат SSL сертификата: как конвертировать сертификат в .PEM, .CER, .CRT, .DER, PKCS ИЛИ PFX?
    MDN
    Surf
    Компания

    Комментарии 6

      0
      Выглядит довольно надёжно, но что если пользователь сам скомпрометирует хранилище сертификатов в своей системе, установив в доверенные сертификат злоумышленника.

      Имхо, это достаточно смелое предположение.


      Отдельный момент — это то, как многие приложения реализуют Pinning: пользователь получает максимально неинформативное сообщение о том, что приложение не работает (крайне мало компаний пишут хоть что-то о том, что сертификат может быть подменён), он пытается ещё раз, идёт искать "ApplicationName не работает", попадает на официальный сайт, где написано "Вышла новая версия приложения, старая вообще никак работать не будет, установите по ссылке новую, в ней дадим 100 рублей компенсации", а там уже ссылка на фишинговое приложение.

        0
        Имхо, это достаточно смелое предположение.

        Извините, а можете пояснить подробнее? Вы в целом алгоритмы SSL и TLS считаете ненадежными, или же ситуацию когда пользователь старается испортить себе жизнь? Если второе, так я вроде это и написал — пользователь сам с удовольствием ищет себе проблемы, поэтому мы и вынуждены реализовывать pinning на уровне приложения.

        А во второй части — полностью с Вами согласен. Разработчикам стоит не только задумываться о безопасности на уровне приложения, но и том, как именно уведомить пользователя о потенциальной атаке. А еще желательно предоставить ему в таком случае инструкцию как поступить в максимально простом виде. Я бы даже сказал, что это продолжение механизма защиты от атаки. Если не объяснить пользователю что он в опасности — он продолжит и обязательно найдет себе неприятности, например таким образом, который вы описали.
        –1
        От митма спасут сами ОС которые не дадут установить контакт если сертификат не подходит/произошел даунгрейд, пытаются подменить.
        Если ваше приложение захотят атаковать надо просто заменить файл сертификата и пересобарать приложение, на андроиде это делалось за пару минут.
        А что вы собираетесь делать если истечет срок жизни сертификата, о таких вещах частенько забывают?
          0
          От митма спасут сами ОС которые не дадут установить контакт если сертификат не подходит/произошел даунгрейд, пытаются подменить.

          Тут вы скорее неправы, ОС спасут только в 1 случае — если пользователь не начнет чудить. Допустим он самостоятельно может поставить сертификат злоумышленника себе в систему. К слову, мы недавно проходили пин-тесты в одном из бизнес проектов. Так вот в требованиях по безопасности было четко прописано, что приложение должно корректно защититься в случае если «злоумышленник с помощью методов социальной инженерии убедит пользователя установить скомпрометированный сертификат». И это абсолютно правильно — никогда не надо полагаться на пользователя. Закон Мёрфи — всё, что может пойти не так, пойдёт не так.

          Если ваше приложение захотят атаковать надо просто заменить файл сертификата и пересобарать приложение, на андроиде это делалось за пару минут.

          Вы недостаточно внимательно прочитали статью, я писал об этом. В той реализации которую предлагаю я, для этого придется приложить довольно много усилий, потому что сертификат вшит в код, а не положен в ресурсы. А код в релизной сборке Flutter компилируется AOT способом.

          А что вы собираетесь делать если истечет срок жизни сертификата, о таких вещах частенько забывают?

          С этим соглашусь, тут стоит учитывать время жизни сертификата и подобрать оптимальное решение для вас. Обеспечить ко времени его протухания обновление вашего приложения у пользователей. Или если вы не можете гарантировать подобного, стоит смотреть в направлении установки цепочек доверенных сертификатов. Принцип в целом тот же — вы настраиваете внутри приложения свою среду «доверия/недоверия» не полагаясь на системные настройки.
            0
            Android при установке сертификата в системное хранилище заставляет поставить пин код, и постоянно напоминает, что трафик может мониториться. В полной мере от социальной инженерии невозможно защититься.

            По поводу сертификата буду благодарен за сравнение кода с примером сертификата и кода сертификата как list, в том смысле насколько они разные/узнаваемые и тяжело ли будет их найти.
              0
              В полной мере от социальной инженерии невозможно защититься.

              Именно про это и речь, нельзя полагаться на сознательность пользователя. Поэтому самостоятельное определение того, что является доверенной зоной приложением, а не системой, является частой реализацией пиннинга. Нужно использовать все возможности которые доступны для защиты. Это как с входной дверью — вы можете держать ее открытой, закрыть, или поставить как в банковском хранилище, и в любом случае будет возможность, что к вам проберутся. Но оставлять дверь открытой не самый лучший выход, правда?

              По поводу сертификата буду благодарен за сравнение кода с примером сертификата и кода сертификата как list, в том смысле насколько они разные/узнаваемые и тяжело ли будет их найти.


              Вы можете просто почитать про AOT компиляцию. Если вкратце, то декомпиляция данного кода невозможна, возможно дизассемблирование и при этом придется приложить достаточно много усилий. Да, это не стопроцентная гарантия от взлома, но это уже далеко не простая задача.
              Кстати Flutter использует AOT только для релизной сборки, при debug сборке используется JIT компиляция, и вот оттуда можно довольно легко выдернуть сам код. Это на случай, если вы решите попробовать декомпилировать приложение.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое