首页 > 解决方案 > 如何将 ECDSA DER 编码的签名数据转换为 Microsoft CNG 支持的格式?

问题描述

我正在准备一个微型驱动程序来使用NCryptSignHashMicrosoft CNG 的功能执行登录智能卡。

当我在智能卡中使用 SECP521R1 EC 密钥执行签名时,它会生成长度为 139 的签名数据作为 ECC 签名数据格式:

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

样本签名数据是

308188024201A2001E9C0151C55BCA188F201020A84180B339E61EDE61F6EAD0B277321CAB81C87DAFC2AC65D542D0D0B01C3C5E25E9209C47CFDDFD5BBCAFA0D2AF2E7FD86701024200C103E534BD1378D8B6F5652FB058F7D5045615DCD940462ED0F923073076EF581210D0DD95BF2891358F5F743DB2EC009A0608CEFAA9A40AF41718881D0A26A7F4

但是当我使用MS_KEY_STORAGE_PROVIDER它执行符号时,它会生成一个长度为 132 字节的符号。

将符号数据大小从 139 减少到 132 的过程是什么?

标签: c++cryptographyelliptic-curvecng

解决方案


您的输入是一个 X9.62 签名格式,它是一个包含两个 ASN.1 / DER 编码签名的序列。这些整数是可变大小的有符号大端数。它们以最少的字节数进行编码。这意味着编码的大小可以变化。

139 字节很常见,因为它假定 和 的最大编码r大小s。这些值是使用算术计算的,因此它们可以包含任意数量的位,最多为 order 的位数n,这与密钥大小相同,即 521 位。


132 个字节由 ISO/IEC 7816-8 / IEEE P1363 指定,这是一个处理智能卡签名的标准。签名由 和 的串联组成rs其中rs被编码为最小字节数,以显示与订单大小相同的值,以字节为单位。r和是静态大小的s、无符号的、大端数。

ror的字节数的计算sceil((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 值不必要地用零字节左填充。

这些问题都不应该破坏算法的安全性,但其他库可能而且应该不那么宽松。


推荐阅读