Как преобразовать данные подписи в кодировке ECDSA DER в формат, поддерживаемый Microsoft CNG?

Я готовлю мини-драйвер для входа в смарт-карту с использованием NCryptSignHash функции Microsoft CNG.

Когда я выполняю подпись с помощью ключа SECP521R1 EC на смарт-карте, он генерирует данные подписи длиной 139 в формате подписанных данных ECC:

ECDSASignature ::= SEQUENCE {
    r   INTEGER,
    s   INTEGER
}

Образец подписанных данных

308188024201A2001E9C0151C55BCA188F201020A84180B339E61EDE61F6EAD0B277321CAB81C87DAFC2AC65D542D0D0B01C3C5E25E9209C47CFDDFD5BBCAFA0D2AF2E7FD86701024200C103E534BD1378D8B6F5652FB058F7D5045615DCD940462ED0F923073076EF581210D0DD95BF2891358F5F743DB2EC009A0608CEFAA9A40AF41718881D0A26A7F4

Но когда я выполняю знак с использованием MS_KEY_STORAGE_PROVIDER, он генерирует знак длиной 132 байта.

Как можно уменьшить размер данных знака со 139 до 132?


person iOS-Developer84    schedule 08.06.2018    source источник
comment
Насколько я понимаю, удаление заголовков DER «3081880242» и среднего «0242» может сделать размер 132. Кто-нибудь может предложить мне любой связанный документ.   -  person iOS-Developer84    schedule 08.06.2018
comment
Ой. У меня было неправильное направление в первой версии этого комментария. Если подпись MS CNG составляет всего 132 байта, то в ней не осталось места для кодирования DER / ASN.1. P-521 требует 66 байтов на координату. Это ASN.1 / DER анализатор удобно: lapo.it/asn1js/   -  person lockcmpxchg8b    schedule 11.06.2018


Ответы (2)


Ваш ввод - это формат подписи X9.62, который представляет собой ПОСЛЕДОВАТЕЛЬНОСТЬ, содержащую две подписи в кодировке ASN.1 / DER. Это целые числа переменного размера со знаком и прямым порядком байтов. Они кодируются минимальным количеством байтов. Это означает, что размер кодировки может быть разным.

139 байтов являются общими, поскольку предполагают максимальный размер кодировки для r и s. Эти значения вычисляются с использованием модульной арифметики, и поэтому они могут содержать любое количество битов, вплоть до количества битов порядка n, которое совпадает с размером ключа, 521 бит.


132 байта указаны в стандарте ISO / IEC 7816-8 / IEEE P1363, который имеет дело с подписями для смарт-карт. Подпись состоит из конкатенации r и s, где r и s закодированы как минимальное количество байтов для отображения значения того же размера, что и порядок, в байтах. r и s представляют собой беззнаковые числа с прямым порядком байтов статического размера.

Вычисление количества байтов r или s равно ceil((double) n / 8) или (n + 8 - 1) / 8, где 8 - количество битов в байте. Таким образом, если эллиптическая кривая составляет 521 бит, то результирующий размер составляет 66 байтов, и, следовательно, вместе они занимают 132 байта.


Теперь о расшифровке. Есть несколько способов справиться с этим: выполнить полный синтаксический анализ ASN.1, получить целые числа и затем снова их закодировать в форме ISO 7816-8, которая является наиболее логичной.

Однако вы также можете видеть, что вы можете просто скопировать байты, поскольку r и s всегда будут неотрицательными (и, следовательно, беззнаковыми) и прямым порядком байтов. Так что нужно просто компенсировать размер. В противном случае единственная сложная часть - это возможность декодировать длину компонентов в структуре X9.62.


Предупреждение: используйте C # вместо C ++, как я ожидал в основном языке .NET; язык, не указанный в вопросе, когда я писал основную часть ответа.

class ConvertECDSASignature
{
    private static int BYTE_SIZE_BITS = 8;
    private static byte ASN1_SEQUENCE = 0x30;
    private static byte ASN1_INTEGER = 0x02;

    public static byte[] lightweightConvertSignatureFromX9_62ToISO7816_8(int orderInBits, byte[] x9_62)
    {
        int offset = 0;
        if (x9_62[offset++] != ASN1_SEQUENCE)
        {
            throw new IllegalSignatureFormatException("Input is not a SEQUENCE");
        }

        int sequenceSize = parseLength(x9_62, offset, out offset);
        int sequenceValueOffset = offset;

        int nBytes = (orderInBits + BYTE_SIZE_BITS - 1) / BYTE_SIZE_BITS;
        byte[] iso7816_8 = new byte[2 * nBytes];

        // retrieve and copy r

        if (x9_62[offset++] != ASN1_INTEGER)
        {
            throw new IllegalSignatureFormatException("Input is not an INTEGER");
        }

        int rSize = parseLength(x9_62, offset, out offset);
        copyToStatic(x9_62, offset, rSize, iso7816_8, 0, nBytes);

        offset += rSize;

        // --- retrieve and copy s

        if (x9_62[offset++] != ASN1_INTEGER)
        {
            throw new IllegalSignatureFormatException("Input is not an INTEGER");
        }

        int sSize = parseLength(x9_62, offset, out offset);
        copyToStatic(x9_62, offset, sSize, iso7816_8, nBytes, nBytes);

        offset += sSize;

        if (offset != sequenceValueOffset + sequenceSize)
        {
            throw new IllegalSignatureFormatException("SEQUENCE is either too small or too large for the encoding of r and s"); 
        }

        return iso7816_8;
    }

    /**
     * Copies an variable sized, signed, big endian number to an array as static sized, unsigned, big endian number.
     * Assumes that the iso7816_8 buffer is zeroized from the iso7816_8Offset for nBytes.
     */
    private static void copyToStatic(byte[] sint, int sintOffset, int sintSize, byte[] iso7816_8, int iso7816_8Offset, int nBytes)
    {
        // if the integer starts with zero, then skip it
        if (sint[sintOffset] == 0x00)
        {
            sintOffset++;
            sintSize--;
        }

        // after skipping the zero byte then the integer must fit
        if (sintSize > nBytes)
        {
            throw new IllegalSignatureFormatException("Number format of r or s too large");
        }

        // copy it into the right place
        Array.Copy(sint, sintOffset, iso7816_8, iso7816_8Offset + nBytes - sintSize, sintSize);
    }

    /*
     * Standalone BER decoding of length value, up to 2^31 -1.
     */
    private static int parseLength(byte[] input, int startOffset, out int offset)
    {
        offset = startOffset;
        byte l1 = input[offset++];
        // --- return value of single byte length encoding
        if (l1 < 0x80)
        {
            return l1;
        }

        // otherwise the first byte of the length specifies the number of encoding bytes that follows
        int end = offset + l1 & 0x7F;

        uint result = 0;

        // --- skip leftmost zero bytes (for BER)
        while (offset < end)
        {
            if (input[offset] != 0x00)
            {
                break;
            }
            offset++;
        }

        // --- test against maximum value
        if (end - offset > sizeof(uint))
        {
            throw new IllegalSignatureFormatException("Length of TLV is too large");
        }

        // --- parse multi byte length encoding
        while (offset < end)
        {
            result = (result << BYTE_SIZE_BITS) ^ input[offset++];
        }

        // --- make sure that the uint isn't larger than an int can handle
        if (result > Int32.MaxValue)
        {
            throw new IllegalSignatureFormatException("Length of TLV is too large");
        }

        // --- return multi byte length encoding
        return (int) result;
    }
}

Обратите внимание, что код в некоторой степени разрешающий, поскольку он не требует кодирования минимальной длины для кодирования длины SEQUENCE и INTEGER (что должно).

Это также позволяет неправильно закодировать значения типа INTEGER, которые излишне дополняются слева нулевыми байтами.

Ни одна из этих проблем не должна нарушать безопасность алгоритма, но другие библиотеки могут и должны быть менее разрешительными.

person Maarten Bodewes    schedule 11.06.2018
comment
Упс, я думал, что читаю C #, плохо. А можно попробовать самому перенести его на C ++? Я предположил C #, потому что CNG - это библиотека .NET и поэтому часто не используется C ++. - person Maarten Bodewes; 11.06.2018
comment
Я думал, что в этом вопросе тоже есть тег C #. Вот почему я добавил несколько комментариев .Net. Кстати, у меня закончились голоса за, поэтому я должен вам один за этот пост. - person jww; 11.06.2018
comment
@jww Извини, это был я. Я добавил тег, потому что в нем был только тег cng, библиотека .NET для криптографических операций. - person Maarten Bodewes; 12.06.2018
comment
@MaartenBodewes Большое спасибо за много информации. Я также написал свой код почти так же, как ваш пример кода для декодирования. - person iOS-Developer84; 12.06.2018
comment
Круто, рад, что мне помогли. Не стесняйтесь добавить C ++ к моему ответу, чтобы другие могли также насладиться вариантом C ++. - person Maarten Bodewes; 12.06.2018

Как можно уменьшить размер данных знака со 139 до 132?

У вас есть подпись в кодировке ASN.1 (см. Ниже). Он используется Java, OpenSSL и некоторыми другими библиотеками. Вам нужна подпись в формате P1363, который представляет собой конкатенацию r || s, без кодировки ASN.1. P1363 используется Crypto ++ и некоторыми другими библиотеками. (Есть еще один распространенный формат подписи - OpenPGP).

Для конкатенации r || s оба r и s должны быть 66 байтов из-за размера элемента поля secp-521r1 на границе октета. Это означает, что процедура состоит в том, чтобы удалить внешний SEQUENCE, а затем удалить два INTEGER, а затем объединить значения двух целых чисел.

Ваша отформатированная r || s подпись с использованием ваших демонстрационных данных будет иметь следующий вид:

01 A2 00 1E ... 7F D8 67 01 || 00 C1 03 E5 ... 0A 26 A7 F4

В Microsoft .Net 2.0 есть классы ASN.1, которые позволяют манипулировать данными в кодировке ASN.1. См. класс AsnEncodedData.


$ echo 08188024201A2001E9C0151C55BCA188F201020A84180B339E61EDE61F6EAD0B277321CAB
81C87DAFC2AC65D542D0D0B01C3C5E25E9209C47CFDDFD5BBCAFA0D2AF2E7FD86701024200C103E5
34BD1378D8B6F5652FB058F7D5045615DCD940462ED0F923073076EF581210D0DD95BF2891358F5F
743DB2EC009A0608CEFAA9A40AF41718881D0A26A7F4 | xxd -r -p > signature.bin

$ dumpasn1 signature.bin
  0 136: SEQUENCE {
  3  66:   INTEGER
       :     01 A2 00 1E 9C 01 51 C5 5B CA 18 8F 20 10 20 A8
       :     41 80 B3 39 E6 1E DE 61 F6 EA D0 B2 77 32 1C AB
       :     81 C8 7D AF C2 AC 65 D5 42 D0 D0 B0 1C 3C 5E 25
       :     E9 20 9C 47 CF DD FD 5B BC AF A0 D2 AF 2E 7F D8
       :     67 01
 71  66:   INTEGER
       :     00 C1 03 E5 34 BD 13 78 D8 B6 F5 65 2F B0 58 F7
       :     D5 04 56 15 DC D9 40 46 2E D0 F9 23 07 30 76 EF
       :     58 12 10 D0 DD 95 BF 28 91 35 8F 5F 74 3D B2 EC
       :     00 9A 06 08 CE FA A9 A4 0A F4 17 18 88 1D 0A 26
       :     A7 F4
       :   }

0 warnings, 0 errors.

Еще один заслуживающий внимания момент: .Net использует формат XML, подробно описанный в RFC 3275, XML-Signature Syntax and Processing.. Это другой формат, чем ASN.1, P1363, OpenPGP, CNG и другие библиотеки.

Преобразование ASN.1 в P1363 довольно тривиально. Вы можете увидеть пример использования библиотеки Crypto ++ на странице ECDSA подписать с помощью BouncyCastle и подтвердить с помощью Crypto ++.

Вы можете найти криптографическое взаимодействие: цифровые подписи в Code Project полезным .

person jww    schedule 11.06.2018
comment
Поскольку вы используете secp-521r1, r и s должны быть 66 байтов. . Это неверно для сигнатуры X9.62 ASN.1, в которой используются целые числа переменного размера. Однако это максимальный размер, но только потому, что 521 нельзя разделить на 8 (в противном случае может потребоваться дополнительный 00 байт слева, чтобы целочисленная кодировка не интерпретировалась как отрицательное число). - person Maarten Bodewes; 11.06.2018
comment
@MaartenBodewes - Плохо, я имел в виду конкатенацию r || s, а не кодировку ASN.1. Я добавил информацию о размере элемента поля secp-521r1. - person jww; 11.06.2018