如何将 ECDSA DER 编码的签名数据转换为 Microsoft CNG 支持的格式?

How to convert ECDSA DER encoded signature data to microsoft CNG supported format?

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

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

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

样本签名数据是

308188024201A2001E9C0151C55BCA188F201020A84180B339E61EDE61F6EAD0B277321CAB81C87DAFC2AC65D542D0D0B01C3C5E25E9209C47CFDDFD5BBCAFA0D2AF2E7FD86701024200C103E534BD1378D8B6F5652FB058F7D5045615DCD940462ED0F923073076EF581210D0DD95BF2891358F5F743DB2EC009A0608CEFAA9A40AF41718881D0A26A7F4

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

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

What is the procedure to reduce the sign data size from 139 to 132?

您有一个 ASN.1 编码签名(如下所示)。它被 Java、OpenSSL 和其他一些库使用。您需要 P1363 格式的签名,它是 r || s 的串联,没有 ASN.1 编码。 P1363 由 Crypto++ 和一些其他库使用。 (还有一种常见的签名格式,那就是OpenPGP)。

对于 r || s 的串联,rs 都必须是 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 class


$ 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 使用 RFC 3275, XML-Signature Syntax and Processing 中详述的 XML 格式。它与 ASN.1、P1363、OpenPGP、CNG 和其他库的格式不同。

ASN.1 到 P1363 的转换相当简单。您可以在 .

查看使用 Crypto++ 库的示例

您可能会发现代码项目中的 Cryptographic Interoperability: Digital Signatures 很有帮助。

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

139 字节很常见,因为它假定了 rs 编码的最大大小。这些值是使用 modular 算法计算的,因此它们可以包含任意数量的位数,最多为 n 顺序的位数,这与密钥大小相同, 521 位。


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

计算rs的字节数是ceil((double) n / 8)(n + 8 - 1) / 8,其中8是一个字节的位数。因此,如果椭圆曲线为 521 位,则结果大小为 66 字节,因此它们总共消耗 132 字节。


现在开始解码。有多种处理方法:执行完整的 ASN.1 解析,获取整数,然后以 ISO 7816-8 形式再次将它们编码回来是最合乎逻辑的方法。

但是,您还可以看到,您可以简单地将字节复制为 rs 将始终为非负数(因此无符号)和大端。所以你只需要补偿尺寸。否则唯一困难的部分是能够解码 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 值,这些值被不必要地用零字节左填充。

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