【问题标题】:ASN1 encoding routines errors when verifying ECDSA signature type with openssl使用 openssl 验证 ECDSA 签名类型时 ASN1 编码例程错误
【发布时间】:2020-01-24 22:32:18
【问题描述】:

我正在尝试验证外部方提供给我们的 SHA256 ECDSA 数字签名。他们已经在内部验证了他们的签名过程,但我们的尝试没有成功。我们在 openssl 验证期间反复收到 asn1 encoding routines 错误,但我看不出签名或我们的流程有什么问题。

这里是测试设置... 公钥(pubkey.pem):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOorVp0M8xien/r1/1Ln7TkSpzzcX
BL/MGRz66J1HSlEgBD5FwwpO1vo6jf/9azcrrrDdCi2NH9/cSDfv5D8gTA==
-----END PUBLIC KEY-----

被签名的消息是明文字符串:

HELLO

数字签名(signature.sig):

JJhwReHev8cxOsNKCR5t/Ee3WU9c7tkf9RuGNamXdpXQu9OL8ZKnsrblCO7vEmOXGKGrk6NsgA5JZpQhXO3A1Q==

我们采取的一般方法是:

# create message file
echo "HELLO" > hello.txt

#VERIFY
openssl dgst -sha256 -verify pubkey.pem -signature signature.sig hello.txt

反应是

Error Verifying Data
4655195756:error:0DFFF09B:asn1 encoding routines:CRYPTO_internal:too long:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.260.1/libressl-2.6/crypto/asn1/asn1_lib.c:143:
4655195756:error:0DFFF066:asn1 encoding routines:CRYPTO_internal:bad object header:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.260.1/libressl-2.6/crypto/asn1/tasn_dec.c:1113:
4655195756:error:0DFFF03A:asn1 encoding routines:CRYPTO_internal:nested asn1 error:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.260.1/libressl-2.6/crypto/asn1/tasn_dec.c:306:Type=ECDSA_SIG

或者,我们对签名 base64 -D signature.sig > signature.bin 进行了 base64 编码,但得到相同的错误响应。我也尝试过使用openssl pkeyutl,但这也会导致asn1 encoding routines 错误。使用 ans1parse 解析签名产生:

openssl asn1parse -in signature.bin
Error: offset too large

很明显,数字签名的格式我没有处理,但我看不到问题。

【问题讨论】:

    标签: openssl cryptography ecdsa


    【解决方案1】:

    您的 signature.sig 文件似乎是 base64 编码的。像这样解码:

    $ base64 -d signature.sig >signature.bin
    

    让我们看看我们有什么:

    $ hexdump -C signature.bin
    00000000  24 98 70 45 e1 de bf c7  31 3a c3 4a 09 1e 6d fc  |$.pE....1:.J..m.|
    00000010  47 b7 59 4f 5c ee d9 1f  f5 1b 86 35 a9 97 76 95  |G.YO\......5..v.|
    00000020  d0 bb d3 8b f1 92 a7 b2  b6 e5 08 ee ef 12 63 97  |..............c.|
    00000030  18 a1 ab 93 a3 6c 80 0e  49 66 94 21 5c ed c0 d5  |.....l..If.!\...|
    00000040
    

    出于比较目的,我根据您的公钥使用的相同曲线创建了一个新的 ECDSA 私钥 (P-256):

    $ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out key.pem
    

    然后使用它对一些数据进行签名:

    $ echo "HELLO" > hello.txt
    $ openssl dgst -sha256 -sign key.pem -out hello.sig hello.txt
    $ openssl asn1parse -in hello.sig -inform DER
        0:d=0  hl=2 l=  68 cons: SEQUENCE          
        2:d=1  hl=2 l=  32 prim: INTEGER           :2C1599C7765B047A2E98E2265CF6DB91232200559909D7F97CA3E859A39AC02C
       36:d=1  hl=2 l=  32 prim: INTEGER           :14E748DF692A8A7A2E41F984497782FF03F970DDB6591CCC68C71704B959A480
    

    所以你会注意到我们这里有两个整数,每个整数正好是 32 字节长。这对应于 ECDSA_SIG ASN.1 定义:

    ECDSA-Sig-Value ::= SEQUENCE { r INTEGER, s INTEGER }
    

    原始 ECDSA 签名由两个整数“r”和“s”组成。 OpenSSL 期望它们被封装在一个 DER 编码的表示中。但是,正如您已经发现您拥有的签名不是有效的 DER。它但是正好是 64 字节长 - 这表明它由 2 个 32 字节整数连接在一起组成。

    出于本练习的目的,我们可以使用十六进制编辑器将原始 r 和 s 值转换为 DER 格式。让我们看一下我之前创建的 hello.sig 文件的 hexdump:

    $ hexdump -C hello.sig
    00000000  30 44 02 20 2c 15 99 c7  76 5b 04 7a 2e 98 e2 26  |0D. ,...v[.z...&|
    00000010  5c f6 db 91 23 22 00 55  99 09 d7 f9 7c a3 e8 59  |\...#".U....|..Y|
    00000020  a3 9a c0 2c 02 20 14 e7  48 df 69 2a 8a 7a 2e 41  |...,. ..H.i*.z.A|
    00000030  f9 84 49 77 82 ff 03 f9  70 dd b6 59 1c cc 68 c7  |..Iw....p..Y..h.|
    00000040  17 04 b9 59 a4 80                                 |...Y..|
    00000046
    

    我们从30 开始,它告诉我们我们有一个序列。下一个字节是44,这是剩余数据的长度。接下来是02,它是整数的标记,然后是20(等于十进制的32),它是整数的长度。接下来的 32 个字节是整数(r 值)。然后我们有另一个02 字节(整数)和20(长度为32),后跟s 值的32 个字节。

    因此,如果我们将字节 30 44 02 20 添加到二进制签名数据的前面,然后是前 32 个字节的数据,然后是 02 20,然后是接下来的 32 个字节,我们应该得到我们想要的......

    ...除了不幸的是它不是那么简单。您的 s 值存在复杂性。您会注意到它以字节d0 开头。该字节设置了其最重要的位 - 在整数的 DER 编码中表示整数值为负。这不是我们想要的。为了解决这个问题,我们必须在 s 值的前面添加一个额外的 00 字节。

    这样做会改变总长度,因此我们现在必须将这些字节添加到开头的30 45 02 20,然后是签名数据的前 32 个字节,然后是02 21 00,然后是签名数据的下 32 个字节。我在一个十六进制编辑器中做了这个并想出了以下内容:

    $ hexdump -C signature2.bin
    00000000  30 45 02 20 24 98 70 45  e1 de bf c7 31 3a c3 4a  |0E. $.pE....1:.J|
    00000010  09 1e 6d fc 47 b7 59 4f  5c ee d9 1f f5 1b 86 35  |..m.G.YO\......5|
    00000020  a9 97 76 95 02 21 00 d0  bb d3 8b f1 92 a7 b2 b6  |..v..!..........|
    00000030  e5 08 ee ef 12 63 97 18  a1 ab 93 a3 6c 80 0e 49  |.....c......l..I|
    00000040  66 94 21 5c ed c0 d5                              |f.!\...|
    00000047
    

    让我们检查一下这看起来是否正常:

    $ openssl asn1parse -in signature2.bin -inform DER
        0:d=0  hl=2 l=  69 cons: SEQUENCE          
        2:d=1  hl=2 l=  32 prim: INTEGER           :24987045E1DEBFC7313AC34A091E6DFC47B7594F5CEED91FF51B8635A9977695
       36:d=1  hl=2 l=  33 prim: INTEGER           :D0BBD38BF192A7B2B6E508EEEF12639718A1AB93A36C800E496694215CEDC0D5
    

    现在让我们尝试验证签名:

    $ openssl dgst -sha256 -verify pubkey.pem -signature signature2.bin hello.txt
    Verification Failure
    

    该死。如此之近,却又如此之远。但至少我们摆脱了 ASN.1 错误。那么为什么它不起作用呢?凭直觉我这样做了:

    echo -n "HELLO" > hello2.txt
    

    回显的“-n”参数会抑制输出中的换行符。也许换行符不应该包含在要为签名消化的数据中。所以,尝试一下:

    $ openssl dgst -sha256 -verify pubkey.pem -signature signature2.bin hello2.txt
    Verified OK
    

    成功了!

    【讨论】:

    • 您错过了可能需要删除左侧00 值的部分。它们当然不是那种常见的作为一个初始位设置为 1 的值,但它们仍然很常见(第一个字节为 256 中的一个,设置为 @ 的两个字节为 65536 中的 1 987654349@等)
    • 是的 - 好点@MaartenBodewes - 你是完全正确的。如果整数 r 和 s 值具有前导 00 字节,则必须删除它们。
    • 回显的 -n arg 解决了我的问题!感谢您提供如此详尽的答案!
    【解决方案2】:

    您拥有的是所谓的平面签名,由 R 和 S 的值组成 - 因为签名由元组 (R, S) 组成。这些数字被编码为两个大小与密钥大小相同的静态大小、无符号、大端整数。

    但是,OpenSSL 要求 SEQUENCE 中有两个 ASN.1/DER 编码的 INTEGER 值。这是两个动态大小、有符号的大端值(以相同的顺序)。所以你需要重新编码签名才能生效。

    两者之间的转换相对容易,但命令行 OpenSSL 似乎并不直接支持它。所以我建议使用 Perl、Python 或 C 应用程序来执行此操作。


    例如在 Python 3 中(减去文件处理,抱歉):

    from array import array
    import base64
    
    def encodeLength(vsize) -> bytearray:
        tlv = bytearray()
        if (vsize < 128):
            tlv.append(vsize)
        elif (vsize < 256):
            tlv.append(0x81)
            tlv.append(vsize)
        else:
            raise
        return tlv
    
    def encodeInteger(i) -> bytearray:
        signedSize = (i.bit_length() + 8) // 8
        value = i.to_bytes(signedSize, byteorder='big', signed = True)
    
        tlv = bytearray()
        tlv.append(0x02)
        tlv += encodeLength(len(value))
        tlv += value
        return tlv
    
    def encodeSequence(value) -> bytearray:
        tlv = bytearray()
        tlv.append(0x30)
        tlv += encodeLength(len(value))
        tlv += value
        return tlv
    
    # test only
    
    bin = base64.b64decode("JJhwReHev8cxOsNKCR5t/Ee3WU9c7tkf9RuGNamXdpXQu9OL8ZKnsrblCO7vEmOXGKGrk6NsgA5JZpQhXO3A1Q==")
    
    # size of the curve (not always a multiple of 8!)
    keysize = 256
    csize = (keysize + 8 - 1) // 8
    if (len(bin) != 2 * csize):
        raise
    r = int.from_bytes(bin[0:csize], byteorder='big', signed = False)
    s = int.from_bytes(bin[csize:csize * 2], byteorder='big', signed = False)
    
    renc = encodeInteger(r)
    senc = encodeInteger(s)
    rsenc = encodeSequence(renc + senc)
    
    print(base64.b64encode(rsenc))
    

    【讨论】:

    • 查看 Matt 的答案以手动破解此问题,但请注意,签名值可能需要额外的前导 00 值(如果第一个字节 >= 0x80)或删除前导 00 值(如果它们是)现在。
    • 我会试试你的 Python 脚本,但是一旦我们与客户端验证测试签名后,最终结果将是在 Kotlin/BouncyCastle 中编写一个验证器。我相信你们已经给了我足够的力量继续尝试在那里破解这个问题。非常感谢!
    • 在 Java 中有很多地方可以进行这种转换,但是使用 Bouncy 会变得更简单,例如here。请注意,我编写了一个假设命令行的 Python 脚本!下次最好直接问问题。
    • Kotlin 验证器在很长一段时间内不会出现。手头的任务是对该客户端进行快速验证,而 openSSL 感觉是最简单的实用程序。
    猜你喜欢
    • 2020-06-11
    • 2019-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-20
    • 2019-09-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多