【问题标题】:How to convert ECDSA DER encoded signature data to microsoft CNG supported format?如何将 ECDSA DER 编码的签名数据转换为 Microsoft CNG 支持的格式?
【发布时间】:2018-06-08 08:56:14
【问题描述】:

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

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

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

样本签名数据是

308188024201A2001E9C0151C55BCA188F201020A84180B339E61EDE61F6EAD0B277321CAB81C87DAFC2AC65D542D0D0B01C3C5E25E9209C47CFDDFD5BBCAFA0D2AF2E7FD86701024200C103E534BD1378D8B6F5652FB058F7D5045615DCD940462ED0F923073076EF581210D0DD95BF2891358F5F743DB2EC009A0608CEFAA9A40AF41718881D0A26A7F4

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

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

【问题讨论】:

  • 到目前为止,我了解删除 DER 标题 '3081880242' 和中间的 '0242' 可以使大小为 132。任何人都可以建议我任何相关文件。
  • 糟糕。我在此评论的第一个版本中方向错误。如果 MS CNG 签名只有 132 个字节,那么它没有任何空间用于 DER/ASN.1 编码。 P-521 每个坐标需要 66 个字节。这个 ASN.1/DER 解析器很方便:lapo.it/asn1js/…

标签: c++ cryptography elliptic-curve cng


【解决方案1】:

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

139 字节很常见,因为它假定rs 的最大编码大小。这些值是使用算术计算的,因此它们可以包含任意数量的位,最多为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 值不必要地用零字节左填充。

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

【讨论】:

  • 哎呀,我以为我读过 C#,我的错。呃,你能尝试自己将它移植到 C++ 吗?我假设 C#,因为 CNG 是一个 .NET 库,因此 C++ 经常不使用。
  • 我认为这个问题也有 C# 标签。这就是我添加一些.Net cmets 的原因。顺便说一句,我没有投票了,所以我欠你一个帖子。
  • @jww 对不起,那是我。我添加了标签,因为它只有 cng 标签,一个用于加密操作的 .NET 库。
  • @MaartenBodewes 非常感谢您提供的大量信息。我也编写了我的代码,几乎就像你要解码的示例代码一样。
  • 酷,很高兴得到帮助。随意将 C++ 添加到我的答案中,以便其他人也可以享受 C++ 变体。
【解决方案2】:

将符号数据大小从 139 减少到 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 的转换相当简单。您可以在ECDSA sign with BouncyCastle and verify with Crypto++ 看到使用 Crypto++ 库的示例。

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

【讨论】:

  • "因为你使用的是secp-521r1,所以r和s都必须是66字节。".这对于使用可变大小整数的 X9.62 ASN.1 签名是不正确的。然而,它是最大大小,但这只是因为 521 不能除以 8(否则可能需要在左侧额外增加一个 00 字节,因此整数编码不会被解释为负数)。
  • @MaartenBodewes - 我的错,我指的是r || s 的串联,而不是 ASN.1 编码。我添加了有关 secp-521r1 的字段元素大小的信息。
猜你喜欢
  • 2021-11-07
  • 2021-12-01
  • 1970-01-01
  • 2017-01-16
  • 1970-01-01
  • 2021-12-05
  • 2015-10-02
  • 2020-01-25
  • 1970-01-01
相关资源
最近更新 更多