PyDERASN: как я написал ASN.1 библиотеку с slots and blobs / Хабр

Введение для хабра

Приведённый ниже текст является на самом деле первыми двумя главами моей статьи “ASN.1 простыми словами”. Так как сама статья достаточно большая по меркам Хабра я решил сначала проверить являются ли знания по кодированию простых типов востребованными на этом ресурсе. В случае положительной реакции аудитории я продолжу публикацию всех остальных глав.

Введение

Уже на протяжение достаточно большого периода мне приходится иметь дело с ASN.1. Мне посчастливилось работать как в сфере создания криптографических программ, так и в сфере телекоммуникаций. И в той, и в другой сфере изначально крайне активно и повсеместно используется стандарт ASN.1.

Однако и в процессе создания программ криптографической направленности, и в процессе создания программ для телекоммуникационной отрасли я постоянно встречался с одним и тем же мнением — ASN.1 это сложный и не понятный формат, а следовательно для кодирования / декодирования лучше применять сторонние компиляторы (а иногда даже другие стандарты кодирования передаваемой информации).

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

Глава 2. кодирование типа real

Общее описание типа:

Для начала немного теории, касающейся собственно чисел с плавающей запятой. Числа с плавающей запятой обычно представляют состоящими из трёх частей: мантиссы, основания и экспоненты. Более просто это можно объяснить с помощью формулы: REAL = (мантисса)*(основание)(экспонента).

Если по этой формуле представлять обычные десятичные числа, то получится REAL = (мантисса)*10(экспонента). Так как в ASN.1 и мантисса и экспонента могут быть как положительными, так и отрицательными, то возможно как представление сколь угодно больших и сколь угодно маленьких значений, с произвольным знаком.

В отличие от обычного, машинного, представления чисел с плавающей запятой (IEEE 754) в ASN.1 тип REAL практически не ограничен по размеру как мантиссы (мантисса может состоять из практически не ограниченного числа октетов и представлять сколь угодно большое число), так и по размеру экспоненты (значение экспоненты также может состоять из произвольного количества октетов).

Для кодирования типа REAL применяются следующие три основных блока:

  1. Служебный информационный октет;
  2. Значение экспоненты числа;
  3. Значение мантиссы числа;

В служебной информационном октете содержится следующая информация:

  • Возможные комбинации битов 8 и 7 (крайние слева):
  • Бит 7 установлен в 0 когда кодируемое число положительно, и установлен в 1 когда кодируемое число отрицательно;
  • Комбинация битов 6 и 5 определяет базу двоичного кодирования:
  • Биты 4 и 3 кодируют значение “scaling factor” (F, см. далее) в двоичном коде;
  • Биты 2 и 1 кодируют формат представления экспоненты в закодированном числе:

Значение экспоненты числа кодируется целым числом, состоящим из произвольного количества октетов. Здесь необходимо сделать маленькое отступление и рассказать как именно в ASN.1 кодируются как положительные целые числа, так и отрицательные.

Положительные целые числа в ASN.1 представляют собой последовательность “индексов” при соответствующих степенях разложения по основанию 256. То есть целое число, представленное в обычном десятичном формате, сначала раскладывается по основанию 256, а потом индексы при соответствующих степенях 256 записываются в качестве кодирующих октетов.

Для наглядного примера возьмём число 32639. Данное число разлагается по основанию 256 как: 3263910 = 127*2561 127*2560. Следовательно коэффициенты при соответствующих степенях 256 будут равны (127, 127). Представляя десятичное значение 127 в виде последовательности битов получаем:

Рассмотренным выше способом можно закодировать сколь угодно большое целое положительное число. Однако как быть с кодированием отрицательных целых значений? Именно для кодирования отрицательных целых применяется специальная процедура кодирования значений.

Для примера опять возьмем число 32639, но теперь пусть оно будет отрицательным (-32639). Кодирование отрицательных целых построено так, что на самом деле кодируется не одно, а два целых значения — одно основное значение и другое целое значение, которое нужно вычесть из основного значения.

То есть при декодировании для получения закодированного отрицательного числа просто вычислить результат (x — y). Как видно из этой простейшей формулы если значение “x” меньше, чем значение “y” то результат будет меньше нуля (то есть отрицательное число).

Вышеупомянутые два числа (основное число и число, которое надо вычесть из основного) формируются по следующим правилам:

Перейдём к кодированию конкретного числа из примера (-32639). Так как число, которое надо вычесть из основного, должно быть больше основного числа, то кодирование отрицательных целых чисел начинается именно с выбора этого вычитаемого. Так как по правилам это вычитаемое должно разлагаться по основанию 256 так, чтобы все биты, представляющие индексы при соответствующих степенях 256, были равны 0 кроме первого бита, то ряд возможных вычитаемых представляет собой лидирующий октет 80 (1000 0000) и какое-то количество октетов 00, следующих за ним.

То есть в качестве вычитаемых могут использоваться: 80 (12810), 80 00 (3276810), 80 00 00 (838860810) и т.п. Для кодирования нашего числа “-32639” выберем первое подходящее вычитаемое, большее кодируемого числа по модулю (то есть большее чем число 32639). Ближайшее такое число равно 32768 (80 00).

Теперь необходимо получить значение основного числа. Для этого надо опять решить простейшую формулу: x — 32768 = -32629. Решая уравнение получаем значение x = 129 = 129*2560, следовательно число 129 кодируется одним байтом 81256.

Так как если более внимательно рассматривать правила то можно понять, что количество бит в основном и вычитаемом числах должно быть равно. Количество бит в вычитаемом равно 16. В то же время количество бит в основном числе равно всего 8. Для увеличения числа бит  в основном числе просто добавим не значащие нули для старших бит.

Тогда получим 129 = 0*2561 129*2560, а следовательно основное число будет кодироваться двумя октетами как (00 81). Теперь устанавливая первый бит в 1 для полученного двух октетного основного числа получаем окончательное число, которое кодирует “-32639”.

Это число будет кодироваться двумя октетами 80 81. Ещё раз — основное число образуется из всех битов закодированного числа, кроме самого старшего бита (получаем что основное число у нас кодируется 00 81), а вычитаемое число образуется только из одного первого бита, установленного в 1, и всех остальных бит, установленных в 0 (получаем, что вычитаемое число у нас кодируется как 80 00).

А теперь приятная информация — в современных компьютерных системах целые числа (как положительные, так и отрицательные) автоматически кодируются и хранятся именно в том формате, который и был описан выше. То есть для кодирования целых чисел в ASN.1 не нужно выполнять вообще никаких действий — просто нужно сохранить их байт за байтом и всё.

Значение мантиссы числа представляет собой всегда без знаковое целое. То есть мантисса числа, кодированного в ASN.1, всегда является положительным числом. Для того чтобы кодировать отрицательные числа с плавающей точкой в ASN.1 предусмотрен отдельный бит (бит 7) в служебном октете (см. выше).

Читайте также:  «Как проверить цифровую подпись в личном налоговом кабинете и процесс получения электронной подписи в качестве физического лица для целей, связанных с налогообложением»

Мантисса кодируется как последовательность байт представляющих собой коэффициенты разложения начального числа по основанию 256. То есть если мантисса числа в десятичном виде равна 32639 то значит закодированное число будет состоять из двух октетов 7F 7F (3263910 = 127*2561 127*2560 = 7F*FF1 7F*FF0).

Примеры кодирования чисел REAL в ASN.1 в двоичном представлении:

  1. Для примера возьмём число 0.15625. Для начала закодируем его в двоичном представлении по основанию 2. Коэффициенты разложения этого числа по основанию 2 будут находится как: 0.1562510 = 1*2-3 1*2-5. То есть мантисса для нашего тестового числа будет иметь значение М = 1012, а значение экспоненты будет равно -5. Служебный октет для этого числа будет 1000 00002 = 8016. Значение экспоненты будет кодироваться одним октетом: -5 = 123 — 128 и следовательно основное число будет равно 12310 = 7B16, а вычитаемое число равно 12810 = 8016. Тогда окончательный октет, кодирующий число -5, будет равен FB256. Значение мантиссы также кодируется одним октетом: 1012 = 0516. Теперь нам известны все части блока, кодирующего значение 0.15625 в двоичном коде по основанию 2 и весь кодирующий блок будет состоять из трёх октетов (80 FB 05)256.
  2. Теперь закодируем это же число 0.15625, но уже по основанию 8. Коэффициенты разложения этого числа по основанию 8 будут находится как: 0.1562510 = 1*8-1 2*8-2. То есть мантисса для нашего тестового числа будет иметь значение М = 128 = (001 010)2 (при кодировании числа в 8-миричной системе для каждого значения требуется три отдельных бита). Значение экспоненты будет равно -2. Служебный октет для этого числа будет 1001 00002 = 90256. Значение экспоненты будет кодироваться одним октетом, где основное и вычитаемое число находятся из формулы: -2 = 126 — 128. Следовательно октет, кодирующий значение экспоненты -2, будет FE256. Значение мантиссы числа будет также кодироваться одним октетом 0A256.
  3. В этом примере разложим число 0.15625 по основанию 16. Коэффициенты этого разложения будут находится как: 0.1562610 = 2*16-1 8*16-2. Следовательно получаем выражение для мантиссы М = 2816 = (0010 1000)2 и значение экспоненты Е = -2. Теперь поставим дополнительное условие: значение мантиссы должно быть “нормализовано”, то есть не должно содержать нулей в младших разрядах числа (также это требование зачастую звучит как “мантисса должна быть нечетной”, так как если крайний младший бит равен 1, то всё число получается нечетным ввиду того, что к степеням двойки добавляется 1*20). Как может быть выполнено подобное условие “нормализации”? Очевидно, что основной способ — изменение значения экспоненты числа, сдвигающее плавающую точку. В случае использования разложения по основанию 2 всё представляется простым — изменение значения экспоненты на 1 сдвигает плавающую точку (или добавляет/удаляет нули в младших разрядах мантиссы) ровно на одну позицию. Однако в случае использования разложения по основаниям 8 и 16 получаем, что изменение значения экспоненты на 1 сдвигает плавающую точку в мантиссе сразу на 3 и 4 бита соответственно (так как в случае разложения по основанию 8 для представления числа требуется 3 бита, а в случае разложения по основанию 16 для представления числа требуется 4 бита). Следовательно далеко не всегда полученное для разложения по основаниям 8 и 16 значение мантиссы может быть “нормализовано” просто изменением значения экспоненты. Для более “тонкой настройки” возможности сдвига плавающей точки в мантиссе был введен дополнительный множитель: умножающий фактор, F. Умножающий фактор сдвигает плавающую точку в мантиссе вправо (или добавляет необходимое количество нулевых бит справа от числа). Для этого перед декодированием значение мантиссы получается как результат умножения M = N * 2F. Общеизвестно, что умножение целого числа на 2 равноценно битовому сдвигу влево на 1 бит. Соответственно умножение на 2F равноценно битовому сдвигу влево на F бит. Таким образом получаем следующий процесс кодирования/декодирования мантиссы при предъявлении требования её нормализации:
    1. Пусть дана мантисса 0010 1000;
    2. При кодировании “нормализуем” её (или сдвигаем вправо на 3 бита), получая 0000 0101, одновременно устанавливая значение умножающего фактора F = 3;
    3. При декодировании умножаем закодированное значение мантиссы на 2F, чем сдвигаем закодированную мантиссу обратно на F = 3 бита влево;

    Следовательно все число с плавающей точкой из нашего примера (при условии “нормализации” мантиссы) будет кодироваться следующей последовательностью октетов:

    AC FE 05

Кроме кодирования всех частей числа с плавающей точкой в виде двоичного представления  в разложении по различным степеням двойки дополнительно есть прекрасная возможность представлять подобные числа в ASN.1 в обычном строковом виде, в каком мы обычно и видим такие числа. В этом случае считается, что число кодируется с основанием 10.

При кодировании по основанию 10 дополнительно вводится понятие “форм представления числа”. Всего таких форм 3 (формы NR1, NR2 и NR3) и описываются они в отдельном стандарте ISO 6093. Так как этот стандарт является платным, то для ознакомления с формами представления чисел можно порекомендовать “предка” ISO 6093 — стандарт ECMA-63, который легко может быть найден в Интернете.

При кодировании числа с плавающей точкой в представлении разложения по основанию 10 в служебном информационном октете указывается код  формы представления числа (01, 02 или 03 для соответствующих форм), а сразу после служебного информационного октета указываются коды символов, представляющих кодированное число. Разрешены следующие коды символов:

  1. Символы, обозначающие цифры 0-9 (коды 30-39 соответственно);
  2. Пробел (код 20);
  3. Разделительный символ “.” (код 2E);
  4. Разделительный символ “,” (код 2C);
  5. Символ представление экспоненты “E” (код 45), либо другой символ представления экспоненты “e” (код 65);
  6. Символ “-” (код 2D);
  7. Символ ” ” (код 2B);

Все остальные символы запрещены к кодированию (при декодировании символов, отличных от приведенных выше, декодер ASN.1 обязан выдать ошибку).

Примеры кодирования числа с плавающей точкой в десятичной форме:

  1. Для примера закодируем обычное число 1. В случае представления в форме NR1 число будет кодироваться строкой “1” (или ” 1″).
  2. В случае представления числа в форме NR2 число уже должно быть закодировано с указанием разделительного символа, поэтому все представленные ниже строки равноценны:
    1. “1,”
    2. ” 1.0″
    3. “1,000000”
    4. ” 1.0″ (в начале строки может присутствовать неограниченное количество пробелов)

  3. Теперь представим 1 в форме NR3. Здесь уже обязательно применение как разделительного символа, так и символа экспоненты. В форме NR3 по стандарту 1 может представляться в виде ” 1,0Е 0″ (“1.0Е 0” в случае разделительного символа “.”), то есть значение экспоненты всегда должно быть нулевым.

Кроме обычных чисел ASN.1 позволяет кодировать также и ряд “специальных” чисел:

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

UPDATE: список последующих глав моей статьи

  1. ASN.1 простыми словами (часть 2)
  2. ASN.1 простыми словами (часть 3, заключительная)

UPDATE #2: Ссылка на файл примеров кодирования для всех типов данных

UPDATE #3: Возможно кто-то упустил, но вот тут находится реализация на С ASN.1 coder/decoder с поддержкой типа REAL. А вот тут реализация на JavaScript, но пока без типа REAL.

Другие библиотеки

Это не было целью, но PyDERASN получился существенно

чем pyasn1. Например, декодирование CRL файлов мегабайтных размеров может занимать настолько продолжительное время, что придётся думать про промежуточные форматы хранения данных (быстрых) и менять архитектуру приложений. pyasn1 декодирует CRL

на моём ноутбуке более 20 минут, тогда как PyDERASN всего за 28 секунд! Есть проект

, нацеленный на быструю работу с криптографическими структурами: он декодирует (полностью, не лениво) этот же самый CRL за 29 секунд, однако потребляет почти в два раза больше оперативной памяти при запуске под Python3 (983 MiB против 498-ми), и в 3.

asn1crypto, который я упомянул, мы не рассматривали, потому что проект ещё только зарождался, и мы не слышали про него. Сейчас бы тоже не стали смотреть в его сторону, так как мною сразу обнаружилось, что тот же GeneralizedTime он не принимает произвольного вида, а при сериализации он молча убирает доли секунды. Это приемлемо для работы с X.509 сертификатами, но в общем случае не подойдёт.

На данный момент, PyDERASN самый строгий из свободных Python/Go DER-декодеров мне известных. В encoding/asn1 библиотеке мною любимого Go не строгая проверка OBJECT IDENTIFIER и UTCTime/GeneralizedTime строк. Иногда строгость может помешать (в первую очередь, из-за обратной совместимости со старыми приложениями, которые никто не будет исправлять), поэтому в PyDERASN во время декодирования можно передавать различные настройки ослабляющие проверки.

Код проекта старается быть максимально простым. Вся библиотека — один файл. Код написан с упором на простоту понимания, без излишних оптимизаций производительности и DRY-кода. В нём нет, как уже говорил, поддержки полноценного BER-декодирования UTCTime/GeneralizedTime строк, а также REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING типов данных. Во всех остальных случаях лично я не вижу смысла использовать в Python другие библиотеки.

Как и все мои проекты, типа PyGOST, GoGOST, NNCP, GoVPN, PyDERASN является полностью свободным ПО, распространяемым на условиях LGPLv3 , и доступен для бесплатного скачивания. Примеры использования есть тут и в тестах PyGOST.

Сергей Матвеев, шифропанк, член Фонда СПО, Python/Go-разработчик, главный специалист ФГУП «НТЦ „Атлас“.

Ошибка проверки pkcs7: поврежденные данные asn1 – reddeveloper

Я не знаю, как вы проверили подпись, кроме бессмысленной проверки в вашем коде, которая повторяет ошибку (ошибки), но это были неправильные данные, и, поскольку вы не показали нам AlgoritmoAssinatura, возможно, также был неправильный метод. У вас также есть несколько других ошибок. Вместо того, чтобы подробно описывать их все, вот пример, который дает допустимый результат с комментариями к изменениям:

// use test data for example
KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream (args[0]), args[1].toCharArray());
PrivateKey PrivKey = (PrivateKey) ks.getKey (args[2], args[1].toCharArray());
X509Certificate Certif = (X509Certificate) ks.getCertificate(args[2]);
String Message = "test";
String ArquivoAssinar = args[3];
String Charset = "ASCII"; // no idea, see below

String SrtResultPKCS7 = "";
byte[] Conteudo = Message.getBytes(Charset);
byte[] Hash;
//String DadosArq = "";
//String Linha = "";
//boolean AssinValid = false;

try {
    // the name in SignerInfo is the _Issuer_ name NOT the Subject
    X500Name xName = X500Name.asX500Name(Certif.getIssuerX500Principal());
    BigInteger serial = Certif.getSerialNumber();
    AlgorithmId digestAlgorithmId = new AlgorithmId(AlgorithmId.SHA_oid);
    AlgorithmId signAlgorithmId = new AlgorithmId(AlgorithmId.RSAEncryption_oid);

    MessageDigest MessDig = MessageDigest.getInstance("SHA1");
    Hash = MessDig.digest(Conteudo);

    PKCS9Attribute Atributo1 = new PKCS9Attribute(PKCS9Attribute.CONTENT_TYPE_OID, ContentInfo.DATA_OID);
    PKCS9Attribute Atributo2 = new PKCS9Attribute(PKCS9Attribute.MESSAGE_DIGEST_OID, Hash); 
    PKCS9Attributes ConjuntoAtrib = new PKCS9Attributes(new PKCS9Attribute[] {Atributo1, Atributo2}); 

    // when using signedattrs, signature is of the encoded attrs 
    // (without the context-implicit tag used when embedded in SignerInfo)
    Signature Sign = Signature.getInstance("SHA1withRSA");
    Sign.initSign(PrivKey);
    Sign.update(ConjuntoAtrib.getDerEncoding());
    byte[] ResultadoAssinatura = Sign.sign();

    SignerInfo sInfo = new SignerInfo(xName, serial, digestAlgorithmId, ConjuntoAtrib, signAlgorithmId, ResultadoAssinatura, null);
    // contenttype inside signed-data is data not digested-data  
    ContentInfo cInfo = new ContentInfo(ContentInfo.DATA_OID, new DerValue(DerValue.tag_OctetString, Conteudo));

    PKCS7 p7 = new PKCS7(new AlgorithmId[] { digestAlgorithmId }, cInfo, new java.security.cert.X509Certificate[] { Certif }, new SignerInfo[] { sInfo });

    ByteArrayOutputStream bOut = new DerOutputStream();
    p7.encodeSignedData(bOut);
    byte[] encoded = bOut.toByteArray();
    // Java doesn't define a class 'Encoder' so I assume this is base64
    SrtResultPKCS7 = DatatypeConverter.printBase64Binary(encoded); // gone in 11!

    FileOutputStream Saida = new FileOutputStream(ArquivoAssinar);
    OutputStreamWriter Escritor = new OutputStreamWriter(Saida, Charset);
    BufferedWriter BuffWriter = new BufferedWriter(Escritor);
    // this was correct for base64 (although the buffering is wasted)
    BuffWriter.write(SrtResultPKCS7);
    // this was nonsense -- it decodes the DER bytes as if they were characters,
    // which they aren't, and then OSW re-encodes them to probably wrong bytes
    //BuffWriter.write(bOut.toString());
    BuffWriter.close();
    // alternatively could write DER/binary with a Stream (NOT a Writer)
}
catch (Exception E) {
    E.printStackTrace();
}

Мне непонятно, какой формат вывода вам нужен – или сайту, на который вы ссылаетесь.
Использование двоичного файла / DER довольно распространено, но его нельзя вырезать и вставить, и с ним труднее работать. Base64 DER встречается редко, но не неизвестен. Если вам или им нужен стандартный формат PEM, используемый множеством программ, это НЕ просто base64 DER; это base64 разрывов строк DER PLUS каждые 64 символа И добавленных строк тире-BEGIN и тире-END.

Кроме того, SHA-1 был нарушен из-за коллизии более года; см. https://shattered.io и многочисленные вопросы по криптографии.SX и security.SX по этому поводу. Еще до этого он был запрещен для подписей многочисленными органами с 2022 или 2022 года, включая NIST (для правительства США) и CABforum (для общедоступных веб-сертификатов). Я ничего не знаю о данных, которые вы подписываете, но если они каким-либо образом важны или ценны, и у вас есть возможность использовать лучший хеш в своей подписи (ах), вам следует это сделать.

ДОБАВЛЕНО: также я предполагаю, что вы понимаете, что классы sun.* не документированы, не гарантированы и могут перестать работать в любое время, когда Oracle сочтет это нужным.

Появление pyderasn

В Атласе мы регулярно, найдя какие-то проблемы или дорабатывая используемые свободные программы, отправляем патчи наверх. В pyasn1 мы несколько раз отправляли доработки, но код pyasn1 не самый простой для понимания и иногда в нём происходили несовместимые изменения API, бившие нас по рукам. Плюс мы привыкли к написанию тестов с генеративным тестированием, чего не было в pyasn1.

В один прекрасный день я решил, что хватить это терпеть и пора попробовать написать собственную библиотеку с __slot__-ами, offset-ами и прекрасно отображаемыми blob-ами! Просто создать ASN.1 кодек было бы недостаточно — нужно перевести все наши друг от друга зависимые проекты на неё, а это сотни тысяч строк кода в которых полно работы с ASN.1-структурами.

То есть одно из требований для неё: лёгкость перевода текущего pyasn1 кода. Потратив весь свой отпуск, я написал эту библиотеку, все проекты перевёл на неё. Так как они имеют практически 100%-ный coverage тестами, то это означало и полную работоспособность библиотеки.

PyDERASN, аналогично, имеет практически 100%-ое покрытие тестами. Используется генеративное тестирование с замечательной библиотекой hypothesis. Также проводился и fuzzingpy-afl-ем на 32-х ядерных машинах. Не смотря на то, что у нас практически не осталось Python2 кода, PyDERASN всё равно блюдёт совместимость с ним и из-за этого имеет единственную six зависимость. Кроме того, он протестирован напротив ASN.1:2008 compliance test suite.

Принцип работы с ним аналогичен pyasn1 — работа с высокоуровненными объектами Python. Описание ASN.1 схем схоже.

class TBSCertificate(Sequence):
    schema = (
        ("version", Version(expl=tag_ctxc(0), default="v1")),
        ("serialNumber", CertificateSerialNumber()),
        ("signature", AlgorithmIdentifier()),
        ("issuer", Name()),
        ("validity", Validity()),
        ("subject", Name()),
        ("subjectPublicKeyInfo", SubjectPublicKeyInfo()),
        ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)),
        ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)),
        ("extensions", Extensions(expl=tag_ctxc(3), optional=True)),
    )

Однако, PyDERASN имеет подобие строгой типизации. В pyasn1 если поле имело тип CMSVersion(INTEGER), то ему можно было присвоить int или INTEGER. PyDERASN жёстко требует чтобы присваиваемый объект был именно CMSVersion. Кроме того, что мы пишем Python3 код, мы используем и

, поэтому в наших функциях будут не непонятные аргументы типа def func(serial, contents), а def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), и PyDERASN помогает блюсти такой код.

При этом в PyDERASN есть крайне удобные поблажки этой самой типизации. pyasn1 не позволял в SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) поле присваивать SubjectKeyIdentifier() объект (без нужного IMPLICIT TAG-а) и приходилось часто копировать и пересоздавать объекты только из-за изменённых IMPLICIT/EXPLICIT тэгов.

Если происходит ошибка во время декодирования, то в pyasn1 не просто понять, где именно она произошла. Например в уже выше упоминавшемся турецком сертификате мы получим вот такую ошибку: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (at 138) unsatisfied bounds:

В первой версии PyDERASN не было поддержки BER-кодирования. Появилась сильно позже и до сих пор ещё не поддерживается обработка UTCTime/GeneralizedTime с часовыми поясами. Это придёт в будущем, ведь проект пишется в основном в свободное от работы время.

Также в первой версии не было работы с DEFINED BY полями. Через несколько месяцев эта возможность появилась и начала активно использоваться, существенно сокращая код приложений — за одну операцию декодирования можно было получить полностью всю структуру разобранную до самой глубины. Для этого, в схеме задаются какие поля что «определяют». Например, описание схемы CMS:

class ContentInfo(Sequence):
    schema = (
        ("contentType", ContentType(defines=((("content",), {
            id_authenticatedData: AuthenticatedData(),
            id_digestedData: DigestedData(),
            id_encryptedData: EncryptedData(),
            id_envelopedData: EnvelopedData(),
            id_signedData: SignedData(),
        }),))),
        ("content", Any(expl=tag_ctxc(0))),
    )

говорит о том, что если contentType будет содержать OID с значением id_signedData, то поле content (находящееся в этом же SEQUENCE) нужно декодировать по схеме SignedData. Почему так много скобочек? Поле может «определять» несколько полей одновременно, как это бывает в EnvelopedData структурах.

Не всегда хочется или не всегда есть возможность сразу же в схему внести эти defines. Могут быть application-specific случаи когда OID-ы и структуры известны только в стороннем проекте. PyDERASN предоставляет возможность задания этих defines прямо в момент декодирования структуры:

Проблемы с pyasn1

На работе мы пишем много программ на Python связанных с криптографией. И несколько лет назад выбора свободных библиотек практически не было: либо это очень низкоуровневые библиотеки, позволяющие просто закодировать/декодировать, например, целое число и заголовок структуры, либо это библиотека

. На ней мы жили несколько лет и поначалу были очень довольны, так как она позволяет работать с ASN.1 структурами как с высокоуровненными объектами: например декодированный объект X.509 сертификата позволяет обращаться к своим полям через интерфейс-словаря:

cert[»tbsCertificate”][«serialNumber»] нам покажет серийный номер этого сертификата. Аналогично, можно «собирать» сложные объекты работая с ними как со списками, словарями, а потом просто вызвать функцию pyasn1.codec.der.encoder.encode и получить сериализованное представление документа.

Однако, вскрывались недостатки, проблемы и ограничения. В pyasn1 были и, к сожалению, до сих пор остаются ошибки: на момент написания статьи, в pyasn1 один из базовых типов — GeneralizedTime, некорректно декодируется и кодируется.

В наших проектах, для экономии места, мы часто храним только путь к файлу, смещение и длину в байтах объекта на который хотим сослаться. Например, произвольный подписанный файл наверняка будет находится в CMS SignedData ASN.1 структуре:

  0     [1,3,1018]  ContentInfo SEQUENCE
  4     [1,1,   9]   . contentType: ContentType OBJECT IDENTIFIER 1.2.840.113549.1.7.2 (id_signedData)
 19-4   [0,0,1003]   . content: [0] EXPLICIT [UNIV 16] ANY
 19     [1,3, 999]   . . DEFINED BY id_signedData: SignedData SEQUENCE
 23     [1,1,   1]   . . . version: CMSVersion INTEGER v3 (03)
 26     [1,1,  19]   . . . digestAlgorithms: DigestAlgorithmIdentifiers SET OF
                           [...]
 47     [1,3, 769]   . . . encapContentInfo: EncapsulatedContentInfo SEQUENCE
 51     [1,1,   8]   . . . . eContentType: ContentType OBJECT IDENTIFIER 1.3.6.1.5.5.7.12.2 (id_cct_PKIData)
 65-4   [1,3, 751]   . . . . eContent: [0] EXPLICIT OCTET STRING 751 bytes OPTIONAL

                 ТУТ СОДЕРЖИМОЕ ПОДПИСЫВАЕМОГО ФАЙЛА РАЗМЕРОМ 751 байт

820     [1,2, 199]   . . . signerInfos: SignerInfos SET OF
823     [1,2, 196]   . . . . 0: SignerInfo SEQUENCE
826     [1,1,   1]   . . . . . version: CMSVersion INTEGER v3 (03)
829     [0,0,  22]   . . . . . sid: SignerIdentifier CHOICE subjectKeyIdentifier
                               [...]
956     [1,1,  64]   . . . . . signature: SignatureValue OCTET STRING 64 bytes
                     . . . . . . C1:B3:88:BA:F8:92:1C:E6:3E:41:9B:E0:D3:E9:AF:D8
                     . . . . . . 47:4A:8A:9D:94:5D:56:6B:F0:C1:20:38:D2:72:22:12
                     . . . . . . 9F:76:46:F6:51:5F:9A:8D:BF:D7:A6:9B:FD:C5:DA:D2
                     . . . . . . F3:6B:00:14:A4:9D:D7:B5:E1:A6:86:44:86:A7:E8:C9

и мы можем достать оригинальный подписанный файл по смещению 65 байт, длиной 751 байт. pyasn1 не хранит этой информации в своих декодированных объектах. Был написан так называемый TLVSeeker — небольшая библиотека, позволяющая декодировать тэги и длины объектов, в интерфейсе которой мы командовали «перейди к следующему тэгу», «войди внутрь тэга» (переходим внутрь SEQUENCE объекта), «перейди к следующему тэгу», «сообщи свой offset и длину объекта, где мы находимся».

Другой недостаток для наших задач pyasn1 — невозможность понять по декодированным объектам, присутствовало ли заданное поле в SEQUENCE или нет. Например, если структура содержит поле Field SEQUENCE OF Smth OPTIONAL, то оно могло полностью отсутствовать в пришедших данных (OPTIONAL), а могло присутствовать, но быть при этом нулевой длины (пустой список).

В общем случае этого нельзя было выяснить. А это необходимо для жёсткой проверки валидности пришедших данных. Представьте, что какой-нибудь удостоверяющий центр выпустил бы сертификат с «не совсем» валидными с точки зрения ASN.1-схем данными! Например удостоверяющий центр «TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı» в своём корневом сертификате вышел за допустимые RFC 5280 границы длины компонента subject — его невозможно честно декодировать по схеме.

DER кодек требует, чтобы поле, у которого значение равно DEFAULT-ному, не кодировалось при передаче — в жизни такие документы встречаются, и первая версия PyDERASN даже осознанно допускала такое невалидное (с точки зрения DER) поведение ради обратной совместимости.

Ещё одно ограничение — невозможность легко узнать, в каком виде (BER/DER) был закодирован тот или иной объект в структуре. Например, CMS стандарт говорит, что сообщение BER-кодируется, но поле signedAttrs, над которым формируется криптографическая подпись, должно быть в DER.

Если мы декодируем DER-ом, то упадём на обработке самой CMS, если декодируем BER-ом, то не узнаем в каком виде был signedAttrs. В итоге придётся TLVSeeker-ом (аналога которого нет в pyasn1) искать местоположение каждого из signedAttrs полей, и его отдельно, достав из сериализованного представления, декодировать DER-ом.

Очень желанной была для нас возможность автоматической обработки DEFINED BY полей, кои встречаются очень часто. После декодирования ASN.1 структуры у нас может остаться множество ANY полей, которые должны быть обработаны дальше по схеме, выбираемой на основе OBJECT IDENTIFIER заданном в поле структуры. В Python коде это означает написание if и дальнейший вызов декодера для ANY поля.

Оцените статью
ЭЦП Эксперт
Добавить комментарий