- Что такое центры сертификации (ca)?
- Что в tls 1.3?
- Что происходит на практике
- Аутентификация клиента (двусторонний TLS)
- Tcpdump: загрузка
- Xt_bpf
- Архитектура машины bpf
- Выпускаем собственные сертификаты
- Краткий курс истории bpf(c)
- Обязательные атрибуты квалифицированных сертификатов
- Пишем и загружаем фильтры для seccomp
- Пишем и загружаем фильтры для seccomp при помощи libseccomp
- Пример посложнее: смотрим на tcp пакеты по порту назначения
- Пример: наблюдаем ipv6 пакеты
- Программируем bpf при помощи собственных рук
- Сертификаты с расширенной проверкой.
Что такое центры сертификации (ca)?
Это организация, которая обладает правом выдачи цифровых сертификатов. Она производит проверку данных, содержащихся в CSR, перед выдачей сертификата. В самых простых сертификатах проверяется только соотвествие доменного имени, в самых дорогих производится целый ряд проверок самой организации, которая запрашивает сертификат. Об этом мы поговорим ниже.
Так вот, разница между самоподписными бесплатным и платными сертификатами, выданными центром сертификации как раз и заключается в том, что данные в сертификате проверены центром сертификации и при использовании такого сертификата на сайте ваш посетитель никогда не увидит огромную ошибку на весь экран.
Говоря в общем, SSL сертификаты содержат и отображают (как минимум одно из) ваше доменное имя, ваше название организации, ваш адрес, город и страницу. Также сертификат всегда имеет дату окончания и данные о центре сертификации, ответственного за выпуск сертификата.
Браузер подключается к защищенному сайту, получает от него SSL сертификат и делает ряд проверок: он не просрочен ли сертификат, потом он проверяет, выпущен ли сертификат известным ему центром сертификации (CA) используется ли сертификат на сайте, для которого он был выпущен.
Если один из этих параметров не проходит проверку, браузер отображает предупреждение посетителю, чтобы уведомить, что этот сайт не использует безопастное соединение SSL. Он предлагает покинуть сайт или продолжить просмотр, но с большой осторожностью.
Центров сертификации существует достаточно много, вот перечень самых популярных:Comodo — работает с 1998 штабквартира в Jersey City, New Jersey, США.Geotrust — основан в 2001, в 2006 продан Verisign, штабквартира Mountain View, California, СШАSymantec — бывший Verisign в состав которого входит и Geotrust.
Как видим самый крупный игрок на рынке SSL сертификатов это Symantec, который владеет тремя крупнейшими центрами сертификации — Thawte, Verisgin и Geotrust.
Что в tls 1.3?
Все упомянутые трудности решаются использованием TLS 1.3. Половины проблем вообще нет, всё проще, красивее, всё прям отличненько. Но TLS 1.3 еще распространен маловато. Самые важные отличия TLS 1.3 (их очень много, они везде, поэтому только самые важные):
- Плохие шифры удалены, остались только хорошие. Инициализировать сессию TLS 1.3 на плохих шифрах не получится. Всё дело в новых cipher suites: тут и другие алгоритмы обмена сеансовых ключей, и так называемый HMAC – не будем углубляться в подробности, потому что Патрик устал. Вкратце: раньше подпись каждого TLS-пакета шла отдельно. На это были атаки, потому что было известно, какой там контент находится. Сейчас ее запихали вовнутрь (режим AEAD), и в TLS 1.3 по-другому быть не может, соответственно, мы избавились от таких атак.
- Handshake стал короче – нет старых сообщений, нет старых расширений, нет возможности по каким-то странным штукам обменяться ключиками. То есть, он тупо короче количественно – даже самый полный TLS 1.3 handshake короче, чем в TLS 1.2.
- Переход на шифрованный канал происходит почти что сразу. Для этого используются разные ключики: да, пока не договорились о хороших ключиках, оптимальных, мы используем какие попало, но канал уже шифрованный. То есть hello – hello и пошло всё зашифрованное. Из-за этого сложнее всё это ломать.
- Всё регламентировано, больше не надо пытаться менять размеры пакетов, забивая их ноликами, чтобы сложнее было расшифровывать.
- Своя пара ключей на каждую сеансовую фазу. Сеансовые ключи меняются: пока мы ни о чем не договорились – они такие, договорились о более крутых – они более крутые, сертификаты проверили и всё хорошо – еще другие ключи. В итоге их много, они усложняются и очень трудно это всё поломать. Еще одна важная вещь, почему это быстрее: Early Data (она же 0-RTT, Zero Round Trip Time) – это когда у тебя в TLS-handshake посылается полезная инфа – ну например GET-запрос. То есть нет такого, что поговорили-поговорили и только потом посылаем что-то отдельным потоком. Сразу же в handshake идет запрос. Как только сервер его получил, он начинает его обрабатывать, отдает клиенту свои данные, сертификат и пока клиент проверяет, сервер уже готов отдать. И может даже в TLS-handshake и отдать иногда.
- Есть pre-shared key, то есть клиент с сервером могут договориться и сохранить сеансовые ключи для последующих соединений. И, соответственно, когда происходит handshake таких вот договорившихся клиентов, на этап выбора ключей время не тратится. Долго объяснять, как это сделано криптографически, но тех атак, которые были на Session ID, вот в этом месте сейчас нет (что хорошо). Всё стало безопаснее и быстрее.
Основная проблема, что поддержка TLS 1.3 – она во всех браузерах, которые актуальны, есть, но не во всех по дефолту включена. Например, в Safari нет (но там очень легко включить), Google Chrome и Mozilla Firefox уже по дефолту поддерживают TLS 1.3. Ngnix с TLS 1.3 – без проблем, в Apache есть нюансы, а вот с почтовыми клиентами хуже — там только Exim молодец, а остальные не очень.
В общем, это наше будущее, там всё получше и попроще, самое главное. Но пока оно еще не везде наступило.
Что происходит на практике
Client Hello – клиент начинает общение с сервером отсылая информацию о предпочитаемой версии протокола TLS, набора поддерживаемых шифров (Cipher Spec), и случайного простого числа (client random), необходимого в дальнейшем для генерации общего ключа симметричного шифрования.
Что такое Cipher Spec? В процессе установки соединения, клиент и сервер должны договориться о: какой алгоритм использовать для обмена ключами (например, RSA – Риверт-Шамир-Адлеман, DH – Диффи-Хеллмана, ECDH – Диффи-Хеллмана на эллиптических кривых, и др.), какой алгоритм использовать для шифрования данных (AES – Advanced Encryption Standard, 3DES – Tripple Data Encryption Algorithm, и др.), какую криптографическую хэш-функцию использовать для генерации Message Authentication Code (SHA-256, SHA-384, SHA-512 – Secure Hash Algorithm с соответствующей длиной строки в битах с хэшем, и др.).
Что такое Message Authentication Code или MAC? Это хэш, сгенерированный с использованием выбранной криптографической хэш-функции и разделяемого ключа, который добавляется сзади к сообщению. Перед отправкой данных отправитель вычисляет MAC для них, а получатель перед обработкой вычисляет MAC для принятого сообщения и сравнивает его с MAC этого принятого сообщения. Предназначен для проверки целостности, то есть что сообщение не было изменено при его передаче.
Server Hello – сервер отвечает выбранной версией протокола и выбранным из предложенного набора шифром, которые будут непосредственно использоваться, своим случайным простым числом (server random) и идентификатором сессии.
Для чего нужен идентификатор сессии? Как мы посмотрим далее, процесс установления TLS соединения затратен по времени и ресурсам. Предусмотрен механизм возобновления соединения с помощью отправки клиентом этого идентификатора. Если сервер тоже все еще хранит соответствующие настройки, то клиент и сервер смогут продолжить общение использую ранее выбранные алгоритмы и ключи.
Certificate – сервер отправляет свой сертификат, а клиент производит проверку подписи удостоверяющего центра, проверку доверия к удостоверяющему центру, проверку указанного домена сайта с фактическим, срока действия, проверяет не был ли сертификат отозван.
Что представляет из себя сертификат? Сертификат – это открытый ключ и другая информация о его владельце, а также Электронная Цифровая Подпись (ЭЦП) доверенного центра.
Как работает ЭЦП? При создании ЭЦП хэш данных, которые подписываются, шифруется закрытым ключом, в отличие от обычного ассиметричного шифрования, где зашифровка выполняется открытым ключом. Таким образом, если вам удалось расшифровать открытым ключом хэш, и он оказался идентичен хэшу из данных, – вы можете быть уверены что: подпись была сделана именно владельцем приватного ключа, открытый ключ которого вы используете; данные, которые были подписаны, не изменились с момента подписания.
Но как удостовериться, что открытый ключ принадлежит не злоумышленнику? Существуют корневые удостоверяющие центры (Root Certificate Authority или просто CA – Certificate Authority), которым доверяют все участники обмена информацией. Если в цепочке подписания сертификата сервера есть подпись корневого CA (мы можем проверить ее с помощью открытого ключа CA), то мы можем ему доверять. При этом сертификаты (открытые ключи) корневых CA распространяются посредством включения их в операционную систему или браузер поставщиками. Также стоит отметить, что сертификат может быть подписан сертификатом, который подписан в свою очередь другим сертификатом – это цепочка подписания.
Кем подписан сертификат корневого CA? А никем, нет инстанции выше корневого CA. Сертификат (открытый ключ) в этом случае подписан собственным закрытым ключом. Такие сертификаты называют самоподписанные (sefl-signed).
Server Key Exchange – этот этап происходит не всегда, только если необходимы дополнительные данные для создания симметричного ключа при выбранном алгоритме. Например, при обмене ключами RSA этот шаг пропускается и для обмена общим ключ передается от клиента серверу зашифрованным открытым ключом сервера из его сертификата. Однако в этой статье рассмотрим более надежный алгоритм Диффи-Хеллмана. Сервер отправляет числа p (большое простое число) и g (может быть маленьким), а также рассчитанное число Ys=gслучайно выбранное сервером числоmod p, где mod – это операция нахождения остатка от деления. В свою очередь клиент также рассчитывает Yc=gслучайно выбранное клиентом числоmod p. После этого сервер считает Ycслучайно выбранное сервером числоmod p, а клиент Ysслучайно выбранное клиентом числоmod p, в результате чего у клиента и сервера получается одинаковое число. Разберем на примере:
Server Hello Done – сервер сообщает, что начальный этап установки соединения завершен
Client Key Exchange – как было уже сказано выше, когда сервер передал числа p, g, Ys в Server Key Echange, клиент передает свое число Yc в Client Key Exchange. Вычисленное в конце общее одинаковое число используется для создания pre-master secret – предварительного разделяемого ключа. На основании client random, server random и pre-master secret псевдослучайная функция выдает симметричный ключ и ключ вычисления MAC. Таким образом клиент и сервер имеют все необходимое для начала обмена полезной информацией.
Change Cipher Spec – клиент говорит серверу, что он готов перейти на защищенное соединение.
Finished – клиент зашифровывает симметричным ключом первое сообщение с MAC.
Change Cipher Spec – сервер проверяет сообщение Finished от клиента и отправляет в ответ свою готовность к защищенному соединению.
Finished – аналогично клиенту, сервер отправляет тестовое зашифрованное сообщение
После этого соединение считается установленным, и происходит передача полезной информации
close_notify – служебное сообщение, которое одна сторона отправляет другой, как уведомление о том, что считаетсоединение разорванным и не будет принимать больше сообщения. Другая сторона в ответ обязана послать аналогичное сообщение close_notify.
Аутентификация клиента (двусторонний TLS)
Следующий шаг нашей работы заключается такой настройке сервера, чтобы он требовал бы аутентификации клиентов. Благодаря этим настройкам мы принудим клиентов идентифицировать себя. При таком подходе сервер тоже сможет проверить подлинность клиента, и то, входит ли он в число доверенных сущностей. Включить аутентификацию клиентов можно, воспользовавшись свойством
client-auth
, сообщив серверу о том, что ему нужно проверять клиентов.
Приведём файл сервера application.yml к такому виду:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:identity.jks
key-password: secret
key-store-password: secret
client-auth: need
Если после этого запустить клиент, то он выдаст следующее сообщение об ошибке:
javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate
Это указывает на то, что клиент не обладает подходящим сертификатом. Точнее — у клиента пока вообще нет сертификата. Поэтому создадим сертификат следующей командой:
keytool -v -genkeypair -dname "CN=Suleyman,OU=Altindag,O=Altindag,C=NL" -keystore client/src/test/resources/identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias client -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth
Нам ещё нужно создать TrustStore для сервера. Но, прежде чем создавать это хранилище, нужно иметь сертификат клиента. Экспортировать его можно так:
keytool -v -exportcert -file client/src/test/resources/client.cer -alias client -keystore client/src/test/resources/identity.jks -storepass secret -rfc
Теперь создадим TrustStore сервера, в котором будет сертификат клиента:
keytool -v -importcert -file client/src/test/resources/client.cer -alias client -keystore shared-server-resources/src/main/resources/truststore.jks -storepass secret -noprompt
Мы создали для клиента дополнительное хранилище ключей, но клиент об этом не знает. Сообщим ему сведения об этом хранилище. Кроме того, клиенту нужно сообщить о том, что включена двусторонняя аутентификация.
Приведём файл application.yml клиента к такому виду:
client:
ssl:
one-way-authentication-enabled: false
two-way-authentication-enabled: true
key-store: identity.jks
key-password: secret
key-store-password: secret
trust-store: truststore.jks
trust-store-password: secret
Сервер тоже не знает о только что созданном для него TrustStore. Приведём его файл
application.yml
к такому виду:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:identity.jks
key-password: secret
key-store-password: secret
trust-store: classpath:truststore.jks
trust-store-password: secret
client-auth: need
Если снова запустить клиент — можно будет убедиться в том, что тест завершается успешно, и что клиент получает данные от сервера в защищённом виде.
Примите поздравления! Только что вы настроили двусторонний TLS!
Tcpdump: загрузка
В предыдущих примерах мы специально не останавливались подробно на том, как именно мы загружаем BPF байткод в ядро для фильтрации пакетов. Вообще говоря, tcpdump портирован на много систем и для работы с фильтрами tcpdump использует библиотеку libpcap. Вкратце, чтобы посадить фильтр на интерфейс при помощи libpcap, нужно сделать следующее:
Для того, чтобы посмотреть как функция pcap_setfilter реализована в Linux, мы используем strace (некоторые строчки были удалены):
$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768) = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...
На первых двух строчках вывода мы создаем raw сокет для чтения всех Ethernet фреймов и привязываем его к интерфейсу eth0. Из нашего первого примера мы знаем, что фильтр ip будет состоять из четырех BPF инструкций, и на третьей строчке мы видим, как при помощи опции SO_ATTACH_FILTER системного вызова setsockopt мы загружаем и подсоединяем фильтр длины 4. Это и есть наш фильтр.
Стоит отметить, что в классическом BPF загрузка и подсоединение фильтра всегда происходит как атомарная операция, а в новой версии BPF загрузка программы и привязка ее к генератору событий разделены по времени.
Чуть более полная версия вывода выглядит так:
$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768) = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0xbeefbeefbeef}, 16) = 0
recvfrom(3, 0x7ffcad394257, 1, MSG_TRUNC, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...
Как было сказано выше, мы загружаем и подсоединяем к сокету наш фильтр на строке 5, но что происходит на строчках 3 и 4? Оказывается, это libpcap
заботится о нас — для того, чтобы в вывод нашего фильтра не попали пакеты, ему не удовлетворяющие, библиотека подсоединяет фиктивный фильтр ret #0
(дропнуть все пакеты), переводит сокет в неблокирующий режим и пытается вычитать все пакеты, которые могли остаться от прошлых фильтров.
Итого, чтобы фильтровать пакеты на Linux при помощи классического BPF, необходимо иметь фильтр в виде структуры типа struct sock_fprog и открытый сокет, после чего фильтр можно присоединить к сокету при помощи системного вызова setsockopt.
Xt_bpf
Отправимся теперь обратно в мир сетей.
Предыстория: давным-давно, в 2007 году, в ядро был добавлен модуль xt_u32 для netfilter. Он был написан по аналогии с еще более древним классификатором трафика cls_u32 и позволял писать произвольные бинарные правила для iptables при помощи следующих простых операций: загрузить 32 бита из пакета и проделать с ними набор арифметических операций. Например,
sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "seen-by-xt_u32"
Загружает 32 бита заголовка IP, начиная с отступа 6, и применяет к ним маску 0xFF (взять младший байт). Это — поле protocol заголовка IP и мы его сравниваем с 1 (ICMP). В одном правиле можно комбинировать много проверок, а еще можно выполнять оператор @ — перейти на X байт вправо. Например, правило
iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"
проверяет, не равен ли TCP Sequence Number 0x29. Не буду дальше вдаваться в подробности, так как уже ясно, что руками такие правила писать не очень удобно. В статье BPF — the forgotten bytecode, есть несколько ссылок с примерами использования и генерации правил для xt_u32. См. также ссылки в конце этой статьи.
Начиная с 2021 года вместо модуля xt_u32 можно использовать основанный на BPF модуль xt_bpf. Всем дочитавшим до сюда уже должен быть ясен принцип его работы: запускать байткод BPF в качестве правил iptables. Создать новое правило можно, например, так:
iptables -A INPUT -m bpf --bytecode <байткод> -j LOG
здесь <байткод> — это код в формате вывода ассемблера bpf_asm по-умолчанию, например,
$ cat /tmp/test.bpf
ldb [9]
jneq #17, ignore
ret #1
ignore: ret #0
$ bpf_asm /tmp/test.bpf
4,48 0 0 9,21 0 1 17,6 0 0 1,6 0 0 0,
# iptables -A INPUT -m bpf --bytecode "$(bpf_asm /tmp/test.bpf)" -j LOG
В этом примере мы фильтруем все UDP пакеты. Контекст для BPF программы в модуле xt_bpf, конечно, указывает на данные пакета, в случае iptables — на начало заголовка IPv4. Возвращаемое значение из BPF программы булево, где false означает, что пакет не совпал.
Архитектура машины bpf
Мы познакомимся с архитектурой по-рабочему, разбирая примеры. Однако для начала все же скажем, что у машины было два доступных для пользователя 32-битных регистра, аккумулятор A и индексный регистр X, 64 байта памяти (16 слов), доступной для записи и последующего чтения, и небольшая система команд для работы с этими объектами.
Общая схема запуска машины следующая. Пользователь создает программу для архитектуры BPF и, при помощи какого-то механизма ядра (например, системного вызова), загружает и подключает программу к какому-то генератору событий в ядре (например, событие — это приход очередного пакета на сетевую карту).
Сказанного выше нам будет достаточно для того, чтобы начать разбирать примеры: мы познакомимся с системой и форматом команд по необходимости. Если же вам хочется сразу изучить систему команд виртуальной машины и узнать про все ее возможности, то можно прочитать оригинальную статью The BSD Packet Filter и/или первую половину файла Documentation/networking/filter.txt из документации ядра.
Мы же переходим к рассмотрению всех существенных примеров применения классического BPF в Linux: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.
Выпускаем собственные сертификаты
Теперь, когда мы разобрали теорию, самое время приступить к практике! Нам понадобятся OpenSSL и keytool (входит в поставку JDK). Для начала создадим сертификат корневого CA, которым будем подписывать запросы на подпись сертификата клиента и сервера. Сгенерируем приватный ключ RSA зашифрованный AES 256 с паролем “password” длиной 4096 бит (меньше 1024 считается ненадежным) в файл CA-private-key.key:
openssl genrsa -aes256 -passout pass:password -out CA-private-key.key 4096
Нет какого-то принятого стандарта расширений для файлов, связанных с сертификатами. Мы будем использовать:
Далее создадим новый запрос на подпись сертификата CA-certificate-signing-request.csr, передавая информацию о субъекте “CN=Certificate authority” (если не указывать ключ -subj вас попросят указать: Сountry (C), Locality (L), Organisation (O), Organisation Unit (OU), Common Name (CN), Email, Challenge password – все поля, кроме CN опциональны), приватный ключ и пароль от него:
openssl req -new -key CA-private-key.key -passin pass:password -subj "/CN=Certificate authority/" -out CA-certificate-signing-request.csr t $3
Так как подписать сертификат другим сертификатом пока нельзя, подпишем запрос его же приватным ключом. Получившейся сертификат CA-self-signed-certificate.pem будет самоподписанным со сроком действия 1 день.
openssl x509 -req -in CA-certificate-signing-request.csr -signkey CA-private-key.key -passin pass:password -days 1 -out CA-self-signed-certificate.pempemE
Теперь у нас есть сертификат, которому в будущем будут доверять наши клиент и сервер. Похожим образом сделаем приватные ключи и запросы на подпись сертификата для них:
openssl genrsa -aes256 -passout pass:password -out Server-private-key.key 4096
openssl req -new -key Server-private-key.key -passin pass:password -subj "/CN=localhost/" -out Server-certificate-signing-request.csrt $3
openssl genrsa -aes256 -passout pass:password -out Client-private-key.key 4096
openssl req -new -key Client-private-key.key -passin pass:password -subj "/CN=Client/" -out Client-certificate-signing-request.csr
Подпишем запросы нашим сертификатом CA. Ключ CAcreateserial отвечает за создание файла (в данном случае CA-self-signed-certificate.srl) , в котором будет храниться серийный номер для следующего подписываемого этим сертификатом запроса. Серийный номер для текущего же сертификата сгенерируется случайно.
openssl x509 -req -in Server-certificate-signing-request.csr -CA CA-self-signed-certificate.pem -CAkey CA-private-key.key -passin pass:password -CAcreateserial -days 1 -out Server-certificate.pemt $4
openssl x509 -req -in Client-certificate-signing-request.csr -CA CA-self-signed-certificate.pem -CAkey CA-private-key.key -passin pass:password -days 1 -out Client-certificate.pem
После этого необходимо создать хранилище ключей с сертификатами (keystore) Server-keystore.p12 для использования в нашем приложении. Положим туда сертификат сервера, приватный ключ сервера и защитим хранилище паролем “password”:
openssl pkcs12 -export -in Server-certificate.pem -inkey Server-private-key.key -passin pass:password -passout pass:password -out Server-keystore.p12
Осталось только создать хранилище доверенных сертификатов (truststore): сервер будет доверять всем клиентам, в цепочке подписания которых есть сертификат из truststore. К сожалению, для Java сертификаты в truststore должны содержать специальный object identifier, а OpenSSL пока не поддерживает их добавление. Поэтому здесь мы прибегнем к поставляемому вместе с JDK keytool:
keytool -import -file CA-self-signed-certificate.pem -keystore Server-truststore.p12 -storetype PKCS12 -storepass password -noprompt
Для удобства, все описанные выше действия упакованы в bash script.
Краткий курс истории bpf(c)
Современная технология BPF — это улучшенная и дополненная версия старой технологии с тем же названием, называемой нынче, во избежание путаницы, classic BPF. На основе классического BPF были созданы всем известная утилита tcpdump, механизм seccomp, а также менее известные модуль xt_bpf для iptables и классификатор cls_bpf.
В современном Linux классические программы BPF автоматически транслируются в новую форму, однако, с пользовательской точки зрения, API остался на месте и новые применения классического BPF, как мы увидим в этой статье, находятся до сих пор. По этой причине, а также потому, что следуя за историей развития классической BPF в Linux, станет яснее как и почему она эволюционировала в современную форму, я решил начать именно со статьи про классический BPF.
В конце восьмидесятых годов прошлого века инженеры из знаменитой Lawrence Berkeley Laboratory заинтересовались вопросом о том, как правильно фильтровать сетевые пакеты на современном для конца восьмидесятых годов прошлого века железе. Базовая идея фильтрации, реализованная изначально в технологии CSPF (CMU/Stanford Packet Filter), состояла в том, чтобы фильтровать лишние пакеты как можно раньше, т.е. в пространстве ядра, так как это позволяет не копировать лишние данные в пространство пользователя.
Однако виртуальные машины для существовавших фильтров были спроектированы для запуска на машинах со стековой архитектурой и на новых RISC машинах работали не так эффективно. В итоге усилиями инженеров из Berkeley Labs была разработана новая технология BPF (Berkeley Packet Filters), архитектура виртуальной машины которой была спроектирована на основе процессора Motorola 6502 — рабочей лошадки таких известных продуктов как Apple II или NES. Новая виртуальная машина увеличивала производительность фильтров в десятки раз по сравнению с существовавшими решениями.
Обязательные атрибуты квалифицированных сертификатов
Состав квалифицированного сертификата регулируется Федеральным законом “Об электронной подписи” от 06.04.2021 N 63-ФЗ и Приказом ФСБ РФ от 27 декабря 2021 г. N 795 “Об утверждении Требований к форме квалифицированного сертификата ключа проверки электронной подписи”.
Несколько раз встречались квалифицированные сертификаты проверки подписи юридических лиц, выпущенные аккредитованным УЦ, в которых в поле Subject – субъект сертификации отсутствовал атрибут L – Местоположение.
Контроль квалифицированных сертификатов нашей системы отвергал данный сертификат и документы с ЭП партнера.
Удостоверяющий центр данную ситуацию комментировал так:
атрибут L для юридических лиц, зарегистрированных в г. Москве, не проставляется согласно 63-ФЗ и Приказа №795
На конкретный пункты Закона и Приказа в УЦ не ссылались.
Собственный повторный анализ юридических аспектов показал:
63-ФЗ от 06.04.2021 “Об электронной подписи”
Статья 14. Сертификат ключа проверки электронной подписи
2. Сертификат ключа проверки электронной подписи должен содержать следующую информацию:
2) фамилия, имя и отчество (если имеется) – для физических лиц, наименование и место нахождения – для юридических лиц или иная информация, позволяющая идентифицировать владельца сертификата ключа проверки электронной подписи;
Статья 17. Квалифицированный сертификат
2. Квалифицированный сертификат должен содержать следующую информацию:
2) фамилия, имя, отчество (если имеется) владельца квалифицированного сертификата – для физического лица, не являющегося индивидуальным предпринимателем, либо фамилия, имя, отчество (если имеется) и основной государственный регистрационный номер индивидуального предпринимателя – владельца квалифицированного сертификата – для физического лица, являющегося индивидуальным предпринимателем, либо наименование, место нахождения и основной государственный регистрационный номер владельца квалифицированного сертификата – для российского юридического лица, либо наименование, место нахождения владельца квалифицированного сертификата, а также идентификационный номер налогоплательщика (при наличии) – для иностранной организации (в том числе филиалов, представительств и иных обособленных подразделений иностранной организации);
Приказ ФСБ РФ от 27 декабря 2021 г. № 795
III. Требования к порядку расположения полей квалифицированного сертификата
5) stateOrProvinceName (наименование штата или области).
В качестве значения данного атрибута имени следует использовать текстовую строку, содержащую наименование соответствующего субъекта Российской Федерации. Объектный идентификатор типа атрибута stateOrProvinceName имеет вид 2.5.4.8;
6) localityName (наименование населенного пункта).
В качестве значения данного атрибута имени следует использовать текстовую строку, содержащую наименование соответствующего населенного пункта. Объектный идентификатор типа атрибута localityName имеет вид 2.5.4.7;
7) streetAddress (название улицы, номер дома).
В качестве значения данного атрибута имени следует использовать текстовую строку, содержащую часть адреса места нахождения соответствующего лица, включающую наименование улицы, номер дома, а также корпуса, строения, квартиры, помещения (если имеется). Объектный идентификатор типа атрибута streetAddress имеет вид 2.5.4.9;
В сертификате партнера в имени субъекта сертификации были указаны: stateOrProvinceName (наименование штата или области), streetAddress (название улицы, номер дома).
И не указано localityName (наименование населенного пункта).
locality – Местонахождение
В Законе 63-ФЗ и Приказе №795 нет информации, что для города Москва не нужно заполнять атрибут locality – Местонахождение.
Наоборот в обоих документах на русском языке применяется термин место нахождения, чему соответствует атрибут localityName ( 2.5.4.7)
После данного разбора удалось найти документ ФСБ РФ «ИЗВЕЩЕНИЕ ОБ ИСПОЛЬЗОВАНИИ АТРИБУТА ИМЕНИ «LOCALITYNAME» ПОЛЯ «SUBJECT» В СТРУКТУРЕ КВАЛИФИЦИРОВАННОГО СЕРТИФИКАТА КЛЮЧА ПРОВЕРКИ ЭЛЕКТРОННОЙ ПОДПИСИ»
Согласно части 2.2 статьи 18 Закона об ЭП для заполнения квалифицированного сертификата в соответствии с частью 2 статьи 17 Закона об ЭП аккредитованный удостоверяющий центр запрашивает и получает из государственных информационных ресурсов, в том числе, выписку из единого государственного реестра юридических лиц в отношении заявителя ‑ юридического лица.
Таким образом, в случае отсутствия в выписке из единого государственного реестра юридических лиц информации о наименовании населенного пункта, но при наличии информации о наименовании муниципального образования, аккредитованному удостоверяющему центру для заполнения квалифицированного сертификата вместо наименования соответствующего населенного пункта следует использовать наименование соответствующего муниципального образования.
На основании изложенного в качестве значения атрибута имени «localityName» поля «subject» в структуре квалифицированного сертификата следует указывать текстовую строку, содержащую наименование соответствующего населенного пункта или соответствующего муниципального образования.
Данное извещение с разъяснениями регулятора, как правильно заполнять атрибут L в квалифицированном сертификате юридического лица, также было направлено в аккредитованный УЦ.
Прошу использовать данную информацию в работе.
Желаю всем участникам PKI удачи и успехов!
Пишем и загружаем фильтры для seccomp
Мы уже умеем писать BPF программы и поэтому посмотрим сначала на программный интерфейс seccomp. Установить фильтр можно на уровне процесса, при этом все дочерние процессы будут ограничения наследовать. Делается это при помощи системного вызова seccomp(2):
seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)
где &filter — это указатель на уже знакомую нам структуру struct sock_fprog, т.е. программу BPF.
Чем же отличаются программы для seccomp от программ для сокетов? Передаваемым контекстом. В случае сокетов, нам передавалась область памяти, содержащая пакет, а в случае seccomp нам передается структура вида
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};
Здесь nr — это номер запускаемого системного вызова, arch — текущая архитектура (об этом ниже), args — до шести аргументов системного вызова, а instruction_pointer — это указатель на инструкцию в пространстве пользователя, которая сделала данный системный вызов. Таким образом, например, чтобы загрузить номер системного вызова в регистр A мы должны сказать
ldw [0]
Для программ seccomp существуют и другие особенности, например, доступ к контексту возможен только по 32-битному выравниванию и нельзя загружать пол-слова или байт — при попытке загрузить фильтр ldh [0] системный вызов seccomp вернет EINVAL.
Проверку загружаемых фильтров выполняет функция seccomp_check_filter() ядра. (Из смешного, в оригинальном коммите, добавляющем функциональность seccomp, в эту функцию забыли добавить разрешение на использование инструкции mod (остаток от деления) и теперь она недоступна для seccomp BPF программ, так как ее добавление сломает ABI.)
В принципе, мы уже знаем все, чтобы писать и читать seccomp программы. Обычно логика программы устроена как белый или черный список системных вызовов, например программа
ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0
проверяет черный список из четырех системных вызовов под номерами 304, 176, 239, 279. Что же это за системные вызовы? Мы не можем сказать точно, так как мы не знаем для какой архитектуры писалась программа. Поэтому авторы seccomp предлагают начинать все программы с проверки архитектуры (текущая архитектура указывается в контексте как поле arch структуры struct seccomp_data). С проверкой архитектуры начало примера выглядело бы как:
ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64
и тогда наши номера системных вызовов получили бы определенные значения.
Пишем и загружаем фильтры для seccomp при помощи libseccomp
Написание фильтров в машинных кодах или для ассемблера BPF позволяет получить полный контроль над результатом, но в то же время иногда предпочтительнее иметь переносимый и/или читаемый код. В этом нам поможет библиотека libseccomp, предоставляющая стандартный интерфейс для написания черных или белых фильтров.
Давайте, например, напишем программу, которая запускает бинарный файл по выбору пользователя, установив, предварительно, черный список системных вызовов из вышеупомянутой статьи (программа упрощена для большей читаемости, полный вариант можно найти тут):
#include <seccomp.h>
#include <unistd.h>
#include <err.h>
static int sys_numbers[] = {
__NR_mount,
__NR_umount2,
// ... еще 40 системных вызовов ...
__NR_vmsplice,
__NR_perf_event_open,
};
int main(int argc, char **argv)
{
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
for (size_t i = 0; i < sizeof(sys_numbers)/sizeof(sys_numbers[0]); i )
seccomp_rule_add(ctx, SCMP_ACT_TRAP, sys_numbers[i], 0);
seccomp_load(ctx);
execvp(argv[1], &argv[1]);
err(1, "execlp: %s", argv[1]);
}
Вначале мы определяем массив sys_numbers из 40 номеров системных вызовов для блокировки. Затем, инициализируем контекст ctx и говорим библиотеке, что мы хотим разрешить (SCMP_ACT_ALLOW) все системные вызовы по-умолчанию (строить черные списки проще).
Затем, один за одним, мы добавляем все системные вызовы из черного списка. В качестве реакции на системный вызов из списка мы запрашиваем SCMP_ACT_TRAP, в этом случае seccomp пошлет процессу сигнал SIGSYS с описанием того, какой именно системный вызов нарушил правила.
Для успешной компиляции программу нужно слинковать с библиотекой libseccomp, например:
cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp
Пример успешного запуска:
Пример посложнее: смотрим на tcp пакеты по порту назначения
Посмотрим как выглядит фильтр, который копирует все TCP пакеты с портом назначения 666. Мы рассмотрим случай IPv4, так как случай IPv6 проще. После изучения данного примера, вы можете в качестве упражнения самостоятельно изучить фильтр для IPv6 (ip6 and tcp dst port 666) и фильтр для общего случая (tcp dst port 666). Итак, интересующий нас фильтр выглядит следующим образом:
$ sudo tcpdump -i eth0 -d ip and tcp dst port 666
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 10
(002) ldb [23]
(003) jeq #0x6 jt 4 jf 10
(004) ldh [20]
(005) jset #0x1fff jt 10 jf 6
(006) ldxb 4*([14]&0xf)
(007) ldh [x 16]
(008) jeq #0x29a jt 9 jf 10
(009) ret #262144
(010) ret #0
Что делают строчки 0 и 1 мы уже знаем. На строке 2 мы уже проверили, что это IPv4 пакет (Ether Type = 0x800) и загружаем в регистр A 24-й байт пакета. Наш пакет выглядит как
14 8 1 1
|ethernet header|ip fields|ttl|protocol|...|
а значит мы загружаем в регистр A поле Protocol заголовка IP, что логично, ведь мы же хотим копировать только TCP пакеты. Мы сравниваем Protocol с 0x6 (IPPROTO_TCP) на строке 3.
На строках 4 и 5 мы загружаем полслова, находящиеся по адресу 20, и при помощи команды jset проверяем, не выставлен ли один из трех флагов — в маске выданной jset очищены три старшие бита. Два бита из трех говорят нам, является ли пакет частью фрагментированного IP пакета, и если да, то является ли он последним фрагментом.
Строчка 6 — самая интересная в этом листинге. Выражение ldxb 4*([14]&0xf) означает, что мы загружаем в регистр X четыре младшие бита пятнадцатого байта пакета, умноженные на 4. Четыре младшие бита пятнадцатого байта — это поле Internet Header Length заголовка IPv4, в котором хранится длина заголовка в словах, поэтому и нужно потом умножить на 4.
Интересно, что выражение 4*([14]&0xf) — это обозначение специальной схемы адресации, которое можно использовать только в таком виде и только для регистра X, т.е. мы не можем сказать ни ldb 4*([14]&0xf) ни ldxb 5*([14]&0xf) (мы можем только указать другой offset, например, ldxb 4*([16]&0xf)).
Таким образом, на строчке 7 мы пытаемся загрузить пол-слова, по адресу (X 16). Вспомнив, что 14 байт занимает заголовок Ethernet, а X содержит длину заголовка IPv4, мы понимаем, что в A загружается порт назначения TCP:
14 X 2 2
|ethernet header|ip header|source port|destination port|
Наконец, на строке 8 мы сравниваем порт назначения с искомым значением и на строчках 9 или 10 возвращаем результат — копировать пакет или нет.
Пример: наблюдаем ipv6 пакеты
Представим, что мы хотим смотреть на все IPv6 пакеты на интерфейсе eth0. Для этого мы можем запустить программу tcpdump с простейшим фильтром ip6:
$ sudo tcpdump -i eth0 ip6
При этом tcpdump скомпилирует фильтр ip6 в байт-код архитектуры BPF и отправит его в ядро (см. подробности в разделе Tcpdump: загрузка). Загруженный фильтр будет запущен для каждого пакета, проходящего через интерфейс eth0.
Оказывается, мы можем легко узнать какой именно байткод отправил в ядро tcpdump при помощи самого tcpdump, если запустим его с опцией -d:
$ sudo tcpdump -i eth0 -d ip6
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 3
(002) ret #262144
(003) ret #0
На нулевой строчке мы запускаем команду ldh [12], которая расшифровывается как «загрузить в регистр A пол-слова (16 бит), находящиеся по адресу 12» и единственный вопрос — это что за память мы адресуем? Ответ заключается в том, что по адресу x начинается (x 1)-й байт анализируемого сетевого пакета.
6 6 2
|Destination MAC|Source MAC|Ether Type|...|
Значит после выполнения команды ldh [12] в регистре A окажется поле Ether Type — тип передаваемого в данном Ethernet-фрейме пакета. На строчке 1 мы сравниваем содержимое регистра A (тип пакета) c 0x86dd, а это и есть интересующий нас тип IPv6.
На строчке 1 кроме команды сравнения есть еще два столбца — jt 2 и jf 3 — метки, на которые нужно перейти в случае удачного сравнения (A == 0x86dd) и неудачного. Итак, в удачном случае (IPv6) мы переходим на строчку 2, а в неудачном — на строчку 3.
Программируем bpf при помощи собственных рук
Познакомимся с бинарным форматом инструкций BPF, он очень простой:
16 8 8 32
| code | jt | jf | k |
Каждая инструкция занимает 64 бита, в которых первые 16 бит — это код команды, потом идут два восьмибитных отступа, jt и jf, и 32 бита для аргумента K, назначение которого меняется от команды к команде. Например, команда ret, завершающая работу программы имеет код 6, а возвращаемое значение берется из константы K. На языке C одна инструкция BPF представляется в виде структуры
struct sock_filter {
__u16 code;
__u8 jt;
__u8 jf;
__u32 k;
}
а целая программа — в виде структуры
struct sock_fprog {
unsigned short len;
struct sock_filter *filter;
}
Таким образом, мы уже можем писать программы (коды инструкций мы, допустим, знаем из [1]). Вот так будет выглядеть фильтр ip6 из нашего первого примера:
struct sock_filter code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 1, 0x000086dd },
{ 0x06, 0, 0, 0x00040000 },
{ 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog prog = {
.len = ARRAY_SIZE(code),
.filter = code,
};
Программу prog мы можем легально использовать в вызове
setsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))
Писать программы в виде машинных кодов не очень удобно, но иногда приходится (например, для отладки, создания юнит-тестов, написания статей на хабре и т.п.). Для удобства в файле <linux/filter.h> определяются макросы-помощники — тот же пример, что и выше, можно было бы переписать как
struct sock_filter code[] = {
BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ETH_P_IPV6, 0, 1),
BPF_STMT(BPF_RET|BPF_K, 0x00040000),
BPF_STMT(BPF_RET|BPF_K, 0),
}
Однако, и такой вариант не очень-то удобен. Так рассудили и программисты ядра Linux и поэтому в директории tools/bpf ядра можно найти ассемблер и дебагер для работы с классическим BPF.
Язык ассемблера очень похож на отладочный вывод tcpdump, но в дополнение мы можем указывать символические метки. Например, вот программа, которая дропает все пакеты, кроме TCP/IPv4:
$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0
По умолчанию ассемблер генерирует код в формате <количество инструкций>,<code1> <jt1> <jf1> <k1>,…, для нашего примера с TCP получится
$ tools/bpf/bpf_asm /tmp/tcp-over-ipv4.bpf
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 6,6 0 0 4294967295,6 0 0 0,
Для удобства C программистов можно использовать другой формат вывода:
$ tools/bpf/bpf_asm -c /tmp/tcp-over-ipv4.bpf
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 3, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 1, 0x00000006 },
{ 0x06, 0, 0, 0xffffffff },
{ 0x06, 0, 0, 0000000000 },
Этот текст можно скопировать в определение структуры типа struct sock_filter, как мы и проделали в начале этого раздела.
Сертификаты с расширенной проверкой.
Это самые дорогие сертификаты и получить их сложнее всего. В таких сертификатах есть так называемый «green bar» — то есть при входе не сайт, где установлен такой сертификат в адресной строке браузера посетителя появится зеленая строка, в которой будет указано название организации, получившей сертификат.
Вот как это выглядит на сайте у Thawte.
Такие сертификаты обладают наибольшим уровнем доверия, среди продвинутых посетителей вашего сайта, поскольку сертификат указывает, что компания реально существует, прошла полную проверку и сайт действительно принадлежит ей.
SSL cертификаты с расширенной проверкой (EV) выпускаются только когда центр сертификации (CA) выполняет две проверки, чтобы убедиться, что организация имеет право использовать определенный домен плюс центр сертификации выполняет тщательную проверку самой организации.
Процесс выпуска сертификатов EV стандартизирован и должен строго соотвествовать правилам EV, которые были созданы на специализированном форуме CA/Browser Forum в 2007 году. Там указаны необходимые шаги, которые центр сертификации должен выполнить перед выпуском EV сертификата:
- Должен проверить правовую, физическую и операционную деятельности субъекта.
- Должен убедиться, что организация соответствует официальным документам.
- Необходимо убедиться, что организация имеет исключительное право на использование домена, указанного в сертификате EV.
- Необходимо убедиться, что организация полностью авторизована для выпуска EV сертификата.
Список того, что конкретно будут проверять такой же как и для сертификатов с проверкой организации.
EV сертификаты используются для всех типов бизнеса, в том числе для государственных и некоммерческих организаций. Для выпуска необходимо 10-14 дней.
Вторая часть правил актуальная для центра сертификации и описывает критерии, которым центр сертификации должен соответствовать перед тем, как получить разрешение на выпуск EV сертификата. Она называется, EV правила аудита, и каждый год происходит проверка на соответствие этим правилам.