使用 openssl 验证 ECDSA 签名类型时 ASN1 编码例程错误
ASN1 encoding routines errors when verifying ECDSA signature type with openssl
我正在尝试验证外部方提供给我们的 SHA256 ECDSA 数字签名。他们已经在内部验证了他们的签名过程,但我们的尝试一直没有成功。我们在 openssl 验证期间反复出现 asn1 encoding routines
错误,但我看不出签名或我们的流程有什么问题。
这是测试设置...
Public 键 (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
很明显,数字签名采用的是我无法处理的格式,但我看不出问题所在。
您拥有的是所谓的平面签名,由 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))
您的 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
出于比较目的,我根据您的 public 密钥使用的相同曲线创建了一个新的 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
echo 的“-n”参数禁止输出中的换行符。也许换行符不应该包含在要为签名消化的数据中。所以,尝试一下:
$ openssl dgst -sha256 -verify pubkey.pem -signature signature2.bin hello2.txt
Verified OK
成功!
我正在尝试验证外部方提供给我们的 SHA256 ECDSA 数字签名。他们已经在内部验证了他们的签名过程,但我们的尝试一直没有成功。我们在 openssl 验证期间反复出现 asn1 encoding routines
错误,但我看不出签名或我们的流程有什么问题。
这是测试设置... Public 键 (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
很明显,数字签名采用的是我无法处理的格式,但我看不出问题所在。
您拥有的是所谓的平面签名,由 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))
您的 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
出于比较目的,我根据您的 public 密钥使用的相同曲线创建了一个新的 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
echo 的“-n”参数禁止输出中的换行符。也许换行符不应该包含在要为签名消化的数据中。所以,尝试一下:
$ openssl dgst -sha256 -verify pubkey.pem -signature signature2.bin hello2.txt
Verified OK
成功!