OpenSSL中使用ECDSA签名类型进行验证时出现ASN1编码例程错误

9
我将尝试验证一份由外部方提供的SHA256 ECDSA数字签名。他们在内部已经验证了其签名过程,但我们的尝试没有成功。我们在openssl verify期间反复收到“asn1编码例程”错误,但我无法确定签名或我们的处理存在何问题。
以下是我们的测试设置... 公钥(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编码 base64 -D signature.sig > signature.bin 但是得到相同的错误响应。 我也尝试使用openssl pkeyutl,但结果也是asn1 encoding routines错误。 使用ans1parse解析签名结果:

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

显然,这个数字签名的格式我无法处理,但我无法看出问题所在。

2个回答

20

你的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

为了比较,我基于与您的公钥相同的曲线(P-256)创建了一个新的ECDSA私钥:

$ 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 -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值,我们就可以得到我们想要的结果...

...但很遗憾,情况并不是那么简单。你会注意到s值以d0字节开头。这个字节的最高位被设置为1 - 在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

echo命令中的“-n”参数可以抑制输出中的换行符。也许在用于签名处理的数据中不应包含换行符。因此,试一试:

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

成功!


1
你忽略了一个细节,可能需要在左侧删除 00 值。当然,它们不像以初始位设置为1的值那样普遍,但它们仍然相当常见(对于第一个字节,256个中有一个,对于两个字节设置为 0000 的情况,65536个中有一个等)。 - Maarten Bodewes
1
是的 - @MaartenBodewes 的观点很好 - 你完全正确。如果整数 r 和 s 值具有前导 00 字节,则必须将其删除。 - Matt Caswell
-n参数对echo命令的使用解决了我的问题!感谢您提供如此清晰明了的答案! - Moulde

3
你所拥有的是所谓的平面签名,由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值(如果存在)。 - Maarten Bodewes
我会尝试运行你的Python脚本,但是一旦我们通过客户验证测试签名,最终结果将是在Kotlin/BouncyCastle中编写一个验证器。我相信你们已经给了我足够的信息来尝试解决这个问题。非常感谢! - romanpilot
有许多Java转换工具可用,但是使用Bouncy库变得更简单,例如这里。请注意,我编写了一个假设命令行运行的Python脚本!下次最好直接问问题。 - Maarten Bodewes
Kotlin验证器还需要一段时间才能到来。手头的任务是与此客户进行快速验证,而openSSL似乎是最简单的实用工具。 - romanpilot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接