在本章中,我们将学习数字签名(digital signatures)以及如何在稍后对内容进行签名和验证签名。数字签名具有许多重要的实际应用,例如签署文档、证书、软件和网络请求和响应,包括金融交易。数字签名对于比特币等加密货币的安全性也很重要。数字签名算法是TLS、SSH 和 IPsec 等安全网络协议中客户端和服务器身份验证的必要构建块。数字签名既用于TLS协议握手,也用于签署X.509证书(X.509 certificates),后者用于TLS中的身份表示和验证。有关X.509证书的更多信息将在【第8章X.509证书和PKI】中提供。TLS协议将在【第4部分“TLS连接和安全通信”】中介绍。数字签名也用于安全消息传递标准,如Pretty Good Privacy(PGP)和 Secure Multipurpose Mail Extensions(S/MIME)。例如,OpenSSL版本由 PGP 数字签名签名。
我们还将概述OpenSSL支持的数字签名算法,并就使用和避免哪些数字签名方案提出建议。在本章的实践部分,我们将学习如何在命令行上对内容进行数字签名,并使用C代码以编程方式验证生成的签名。
第七章 数字签名及其验证技术要求理解数字签名数字签名和MACs之间的区别OpenSSL支持的数字签名算法概述RSA速览DSA速览ECDSA速览EdDSA速览SM2速览你应该选择哪一种数字签名算法?如何生成椭圆曲线密钥对如何在命令行上签名和验证签名如何以编程方式签名实施ec-sign程序运行ec-sign程序如何以编程方式验证签名实施ec-verify程序运行ec-verify程序总结
本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要openssl命令行工具以及一些 openssl
动态库。要构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。
我们将在本章中实施一些示例程序,以练习我们正在学习的内容。这些程序的完整源代码可以在这里找到:
https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter07
数字签名是一组比特,为数字消息的真实性、完整性和不可否认性提供了强有力的加密保证。这些保证意味着什么?让我们来看看:
Authenticity ,真实性
意味着消息来自声称的发送者,前提是只有声称的发送者拥有用于生成签名的私钥。
Integrity ,完整性
意味着消息在传输过程中未被第三方更改。
Non-repudiation,不可否认性
意味着发送者不能否认他们制作了签名,前提是没有其他人可以访问用于制作签名的私钥。
数字签名是使用私钥生成的,私钥可以使用相应的公钥进行验证。因此,数字签名和验证算法被认为是非对称密码算法(asymmetric cryptography algorithms),即使它们不是非对称加密算法。
重要的是要知道,当消息被签名时,通常数字签名算法不会应用于消息本身。相反,签名算法应用于消息摘要(message digest),该摘要由一些加密哈希函数(如SHA-256)生成。密码学家说,这种签名方案是基于哈希和签名(hash-and-sign)范式的。
一个值得注意且有争议的例外是EdDSA签名算法。值得注意的是,它是唯一一种将整个消息而不是摘要作为输入的流行签名算法。这是有争议的,因为EdDSA无论如何都会对输入消息进行内部哈希,尽管其方式比其他签名算法更复杂。
那么,为什么对消息摘要而不是消息本身进行签名呢?原因与会话密钥用于非对称加密的原因相同。非对称加密算法相对较慢,比加密哈希函数慢得多。快速散列算法将可能非常长的输入消息转换为已知长度的短消息摘要,这样慢速数字签名算法就不需要处理大量输入。与签名算法需要处理整个长消息相比,这种工作分离提高了性能。另一个原因是,数字签名算法只能对一个块中有限数量的数据进行签名。将消息拆分为具有多个签名的多个块,并发明一种安全的方法来链接生成的签名并抵抗各种变序攻击,这很不方便。
请注意,有效和高性能(effective and performant)的非对称密码学是如何建立在对称密码学之上的。非对称加密使用对称会话密钥,而数字签名使用消息摘要。因此,在学习非对称(asymmetric)密码学之前,了解对称(symmetric)密码学非常重要。
正如我们所看到的,数字签名与消息认证码(Message Authentication Codes,MAC)有一些共同之处。数字签名和MACs都可以提供身份验证和完整性。然而,有一些重要的区别:
现在我们已经了解了数字签名是什么,让我们来看看OpenSSL提供了什么样的数字签名算法。
在本节中,我们将回顾OpenSSL支持的数字签名算法,并就使用哪些算法给出一些建议。
Rivest-Shamir-Adleman(RSA)算法是由Ronald Rivest、Adi Shamir和Leonard Adleman发明的。Ronald Rivest是发明RC家族对称加密算法和MD家族消息摘要算法的同一个人。RSA算法于1977年首次发表。这是第一个流行的非对称加密算法。
RSA是一种非常通用的算法,因为它既可以加密消息,也可以对消息进行签名。在OpenSSL提供的数字签名算法中,RSA是签名最慢但验证相同安全级别最快的算法。
签名和验证的速度取决于密钥大小。签名速度随着密钥大小的增长而迅速下降。验证速度也会下降,但不会那么快。验证比签名快30-70倍,具体取决于密钥大小。
RSA产生最大的签名。RSA签名的大小与用于生成签名的密钥大小相同。例如,由4096位密钥生成的签名将具有4096位或512字节的大小。其他算法产生的签名要小得多。
RSA的另一个缺点是RSA密钥比更现代的基于椭圆曲线(Elliptic Curve,EC)的算法的密钥长得多。此外,为了获得少量的安全性,必须大幅增加RSA密钥的大小。
RSA是一种相当古老的算法。OpenSSL支持另一种旧算法DSA。
数字签名算法(Digital Signature Algorithm,DSA)是由David W.Kravitz在美国国家安全局(National Security Agency,NSA)工作时发明的。DSA于1991年首次发布,当时NIST提出将DSA用于其数字签名标准(Digital Signature Standard,DSS)。
DSA的安全性依赖于离散对数问题的计算难度。与RSA一样,DSA使用长密钥,如2048位密钥。与RSA一样,DSA密钥的安全级别远低于其长度。DSA密钥恰好与长度相同的RSA密钥具有相同的安全级别。因此,如【第6章“非对称加密和解密”】所示,将RSA密钥长度映射到其安全级别的表也可用于找出DSA密钥的安全性。例如,2048位DSA密钥的安全级别为112位。
1994年,第一个DSS被采纳为FIPS-186,它只允许DSA与SHA-1哈希函数一起使用,密钥长度可达1024位。最新的DSS,2013年的FIPS 186-4,也允许我们将DSA与SHA-224和SHA-256哈希函数一起使用,密钥长度可达3072位。OpenSSL通过上述三个哈希函数和长度非常长的密钥支持DSA。然而,在我看来,你不应该需要长度超过4096位的DSA密钥。
尽管DSA密钥很长,就像RSA密钥一样,但它们产生的签名比密钥长度短得多。DSA的签名长度(以位为单位)约为 4*K_bits
,其中 K_bits
是所用DSA密钥的安全强度。例如,4096位RSA密钥产生512字节的签名,但相同长度的DSA密钥产生70或71字节的签名。
目前,DSA不是一种非常流行的算法,正在被ECDSA取代。
椭圆曲线数字签名算法(Elliptic Curve Digital Signature Algorithm,ECDSA)是使用椭圆曲线密码学(Elliptic Curve Cryptography,ECC)的DSA算法的变体。ECC是一种基于有限域上椭圆曲线代数结构的公钥密码学。ECC是由Neal Koblitz和Victor Miller于1985年发明的。ECDSA由Scott Vanstone于1992年提出,作为对第一个DSS标准的评论。
ECDSA相对于DSA的优势在于,在相同的安全级别下,它的密钥大小更短。椭圆曲线密钥的安全级别大约是密钥长度的一半。例如,224位密钥具有112位安全级别。为了进行比较,具有相同安全级别的DSA密钥必须长达2048位。与DSA一样,ECDSA签名长度(以比特为单位)约为 4*K_bits
,其中 K_bits
是所用ECDSA密钥的安全强度。
生成ECDSA密钥时,必须选择一条曲线。曲线有一个名称,并定义了当基于该曲线生成密钥时,EC密钥的长度。例如,基于NIST P-256曲线生成的密钥长度为256位。OpenSSL支持NIST曲线和Brainpool曲线。NIST曲线由NSA开发,并由NIST标准化。Brainpool曲线是由Brainpool工作组提出的,该工作组是一组密码学家,他们对NIST曲线不满意,因为NIST曲线不是可验证的随机生成的,因此它们可能有意或无意地具有弱安全性。Brainpool 线由德国联邦信息安全办公室(BSI)标准化和使用。Brainpool工作组定义了一种可验证的随机生成椭圆曲线的方法,并声称该工作组使用该方法生成了Brainpool曲线。然而,Daniel J.Bernstein领导的一个研究小组试图验证Brainpool工作组方法,但无法使用所描述的方法生成相同的Brainpool曲线。Bernstein的团队后来提出了他们自己的椭圆曲线和自己的EC签名算法EdDSA。下一节将提供EdDSA曲线的更多详细信息。
尽管存在这些疑问,NIST曲线目前是ECDSA中最受欢迎的曲线。NIST曲线套件包括两条非常快的曲线:P-256曲线和P-224曲线。NIST P-256曲线在签名时比Brainpool P256r1曲线快16倍,在验证签名时快5倍。NIST P-224曲线在安全性上与2048位DSA相当,在签名时大约比DSA-2048快五倍,在验证时快两倍。其他NIST曲线的速度与具有类似安全性的Brainpool曲线相当,这意味着有时NIST曲线更快,而其他时候Brainpool曲线更快。
值得一提的是,ECDSA的标准实现需要一个好的随机数生成器(Random Number Generator,RNG)来生成密钥和签名过程。还有一种ECDSA的替代实现,在签名时不需要随机数据生成,它使用私钥的哈希值而不是随机数据。签名过程中出现错误的RNG可能会导致私钥通过签名泄露。正确实施ECDSA时应小心;否则,实现可能容易受到定时攻击,也可能泄露私钥。OpenSSL曾经有一个易受定时攻击的实现,但该漏洞在2011年得到了修复。
ECDSA是一种非常流行的算法,但它有一个竞争对手:另一种基于EC的签名算法,称为EdDSA。
Edwards曲线数字签名算法(Edwards-curve Digital Signature Algorithm,EdDSA)是另一种使用椭圆曲线的签名算法。它是由Daniel J.Bernstein领导的一组密码学家于2011年发表的,Daniel J.Bernstein也是开发ChaCha对称流密码家族和Poly1305 MAC算法的人。
EdDSA基于扭曲的Edwards曲线(twisted Edwards curves),旨在比ECDSA更快、更容易安全地实施。与ECDSA不同,EdDSA在签名过程中不需要RNG,因此不会因为RNG不好而泄露私钥。
EdDSA支持两条曲线:253位Curve25519和456位Curve448。这些曲线是为良好的安全性而设计的,试图使其难以犯可能削弱安全性的实现错误。曲线也不受时间攻击。
与ECDSA一样,EdDSA签名的安全级别是密钥长度的一半,这意味着Curve25519为126.5位,Curve448为228位。Curve25519的签名长度为64字节,而Cruve448的签名长度则为114字节。
尽管Curve25519和Ed448是为速度而设计的,但它们并不比NIST P-256曲线快。NIST P-256的签名和签名验证速度大约是Curve25519的两倍。NIST P-224曲线的速度与Curve25519大致相同。然而,Curve25519在签名方面比其他具有类似安全性的NIST和Brainpool曲线快8-13倍,在验证方面比它们快2-9倍。Curve448在签名方面比NIST和Brainpool曲线快大约两倍,在验证方面快1.5-3倍。
EdDSA是一种不同寻常的数字签名算法。通常的签名算法期望以输入数据的加密哈希的形式输入。为清楚起见,输入数据是要签名的数据。相反,EdDSA期望将整个输入数据作为输入,但允许算法的用户提供所谓的预处理功能,该功能将在进一步处理之前应用于输入数据。
预散列函数可以是实际的加密散列函数,如SHA-256。EdDSA的这种变体称为HashEdDSA。HashEdDSA变体与传统签名算法最为相似,因为它将输入数据的哈希值提供给进一步的处理。预散列函数也可以是恒等函数,即只返回其输入的函数。EdDSA的这种变体被称为PureEdDSA。
值得注意的是,EdDSA算法在使用Curve25519时内部使用SHA-512哈希函数,在使用Curve448时内部使用SHAKE256函数。因此,在HashEdDSA的情况下,输入数据将被散列两次。那么,是否应该始终使用PureEdDSA?不,没那么容易——请继续阅读。
EdDSA与传统签名算法的另一个区别是EdDSA是一种两遍算法,这意味着它对输入数据进行两次处理。这意味着,与传统的签名算法不同,PureEdDSA不支持流式输入数据。如果输入数据很长,并且来自慢速存储或通过昂贵的网络连接请求,PureEdDSA可能不是最佳选择。
OpenSSL 3.0仅支持EdDSA的PureEdDSA变体,并要求将所有数据放入内存进行哈希运算。如果输入数据很长,内存不足,这可能会有问题。另一方面,即使OpenSSL还不直接支持HashEdDSA,程序员也可以先自己对输入数据进行哈希运算,然后使用PureEdDSA进行进一步处理。
现在,让我们来看看我们将要回顾的最后一个签名算法——SM2。
Shang Mi 2(SM2)是另一种基于椭圆曲线的数字签名算法。它是中华人民共和国的国家标准。SM2由王晓云等人发明,并于2012年由中国商用密码管理局(Chinese Commercial Cryptography Administration Office)标准化。
SM2只支持一条曲线:256位CurveSM2。CurveSM2的签名和验证速度与256位Brainpool曲线大致相同。CurveSM2的签名长度为71字节。
SM2签名算法通常与256位SM3哈希函数一起使用。OpenSSL还在其高级API中支持仅将SM2与SM3组合。
正如我们所看到的,OpenSSL支持多种数字签名算法。但你应该选择哪一个?让我们在下一小节中讨论它。
最近,EdDSA算法和Curve25519已经变得相当流行。Curve25519似乎受到许多安全专家的信任。如果要签名的数据始终适合内存,并且不需要与旧软件兼容,请选择EdDSA。值得一提的是,在撰写本文时,主要的证书颁发机构(Certificate Authorities)尚未颁发基于EdDSA的X.509证书。因此,如果你需要为互联网上的服务器提供TLS证书,你需要暂时选择一个用另一种算法签名的证书。
如果你想要一个更传统的签名算法或想要支持流媒体,那么选择ECDSA,它也很受欢迎。
如果您需要与旧软件的互操作性,或者需要非常快速的签名验证,请选择RSA。
至此,我们了解了OpenSSL支持的不同数字签名算法,以及在哪种情况下使用哪种算法的建议。本章的理论部分到此结束。下一节将是实用的。在那里,我们将学习如何生成EC密钥对,以及如何在命令行上对一些数据进行签名。
我想演示在对输入数据的消息摘要进行签名时,数字签名的传统方法。因此,在我们的示例中,我们将使用ECDSA,而不是EdDSA。
正如我们所知,可以使用 openssl genpkey
子命令生成新的密钥对。我们将根据NIST B-571曲线生成OpenSSL中可用的最长长度的EC密钥对,570位:
xxxxxxxxxx
$ openssl genpkey \
-algorithm EC \
-pkeyopt ec_paramgen_curve:secp521r1 \
-out ec_keypair.pem
在这里,我们使用了 -pkeyopt ec_paramgen_curve:secp521r1
开关来指定我们要使用NIST B-571曲线。可以使用哪些其他曲线名称来代替 secp521r1
?使用以下命令可以获得支持曲线的完整列表:
xxxxxxxxxx
$ openssl ecparam -list_curves
secp112r1 : SECG/WTLS curve over a 112 bit prime field
secp112r2 : SECG curve over a 112 bit prime field
secp128r1 : SECG curve over a 128 bit prime field
secp128r2 : SECG curve over a 128 bit prime field
secp160k1 : SECG curve over a 160 bit prime field
secp160r1 : SECG curve over a 160 bit prime field
secp160r2 : SECG/WTLS curve over a 160 bit prime field
secp192k1 : SECG curve over a 192 bit prime field
secp224k1 : SECG curve over a 224 bit prime field
secp224r1 : NIST/SECG curve over a 224 bit prime field
secp256k1 : SECG curve over a 256 bit prime field
secp384r1 : NIST/SECG curve over a 384 bit prime field
secp521r1 : NIST/SECG curve over a 521 bit prime field
prime192v1: NIST/X9.62/SECG curve over a 192 bit prime field
prime192v2: X9.62 curve over a 192 bit prime field
prime192v3: X9.62 curve over a 192 bit prime field
prime239v1: X9.62 curve over a 239 bit prime field
prime239v2: X9.62 curve over a 239 bit prime field
prime239v3: X9.62 curve over a 239 bit prime field
prime256v1: X9.62/SECG curve over a 256 bit prime field
sect113r1 : SECG curve over a 113 bit binary field
sect113r2 : SECG curve over a 113 bit binary field
sect131r1 : SECG/WTLS curve over a 131 bit binary field
sect131r2 : SECG curve over a 131 bit binary field
sect163k1 : NIST/SECG/WTLS curve over a 163 bit binary field
sect163r1 : SECG curve over a 163 bit binary field
sect163r2 : NIST/SECG curve over a 163 bit binary field
sect193r1 : SECG curve over a 193 bit binary field
sect193r2 : SECG curve over a 193 bit binary field
sect233k1 : NIST/SECG/WTLS curve over a 233 bit binary field
sect233r1 : NIST/SECG/WTLS curve over a 233 bit binary field
sect239k1 : SECG curve over a 239 bit binary field
sect283k1 : NIST/SECG curve over a 283 bit binary field
sect283r1 : NIST/SECG curve over a 283 bit binary field
sect409k1 : NIST/SECG curve over a 409 bit binary field
sect409r1 : NIST/SECG curve over a 409 bit binary field
sect571k1 : NIST/SECG curve over a 571 bit binary field
sect571r1 : NIST/SECG curve over a 571 bit binary field
c2pnb163v1: X9.62 curve over a 163 bit binary field
c2pnb163v2: X9.62 curve over a 163 bit binary field
c2pnb163v3: X9.62 curve over a 163 bit binary field
c2pnb176v1: X9.62 curve over a 176 bit binary field
c2tnb191v1: X9.62 curve over a 191 bit binary field
c2tnb191v2: X9.62 curve over a 191 bit binary field
c2tnb191v3: X9.62 curve over a 191 bit binary field
c2pnb208w1: X9.62 curve over a 208 bit binary field
c2tnb239v1: X9.62 curve over a 239 bit binary field
c2tnb239v2: X9.62 curve over a 239 bit binary field
c2tnb239v3: X9.62 curve over a 239 bit binary field
c2pnb272w1: X9.62 curve over a 272 bit binary field
c2pnb304w1: X9.62 curve over a 304 bit binary field
c2tnb359v1: X9.62 curve over a 359 bit binary field
c2pnb368w1: X9.62 curve over a 368 bit binary field
c2tnb431r1: X9.62 curve over a 431 bit binary field
wap-wsg-idm-ecid-wtls1: WTLS curve over a 113 bit binary field
wap-wsg-idm-ecid-wtls3: NIST/SECG/WTLS curve over a 163 bit binary field
wap-wsg-idm-ecid-wtls4: SECG curve over a 113 bit binary field
wap-wsg-idm-ecid-wtls5: X9.62 curve over a 163 bit binary field
wap-wsg-idm-ecid-wtls6: SECG/WTLS curve over a 112 bit prime field
wap-wsg-idm-ecid-wtls7: SECG/WTLS curve over a 160 bit prime field
wap-wsg-idm-ecid-wtls8: WTLS curve over a 112 bit prime field
wap-wsg-idm-ecid-wtls9: WTLS curve over a 160 bit prime field
wap-wsg-idm-ecid-wtls10: NIST/SECG/WTLS curve over a 233 bit binary field
wap-wsg-idm-ecid-wtls11: NIST/SECG/WTLS curve over a 233 bit binary field
wap-wsg-idm-ecid-wtls12: WTLS curve over a 224 bit prime field
Oakley-EC2N-3:
IPSec/IKE/Oakley curve #3 over a 155 bit binary field.
Not suitable for ECDSA.
Questionable extension field!
Oakley-EC2N-4:
IPSec/IKE/Oakley curve #4 over a 185 bit binary field.
Not suitable for ECDSA.
Questionable extension field!
brainpoolP160r1: RFC 5639 curve over a 160 bit prime field
brainpoolP160t1: RFC 5639 curve over a 160 bit prime field
brainpoolP192r1: RFC 5639 curve over a 192 bit prime field
brainpoolP192t1: RFC 5639 curve over a 192 bit prime field
brainpoolP224r1: RFC 5639 curve over a 224 bit prime field
brainpoolP224t1: RFC 5639 curve over a 224 bit prime field
brainpoolP256r1: RFC 5639 curve over a 256 bit prime field
brainpoolP256t1: RFC 5639 curve over a 256 bit prime field
brainpoolP320r1: RFC 5639 curve over a 320 bit prime field
brainpoolP320t1: RFC 5639 curve over a 320 bit prime field
brainpoolP384r1: RFC 5639 curve over a 384 bit prime field
brainpoolP384t1: RFC 5639 curve over a 384 bit prime field
brainpoolP512r1: RFC 5639 curve over a 512 bit prime field
brainpoolP512t1: RFC 5639 curve over a 512 bit prime field
SM2 : SM2 curve over a 256 bit prime field
【debian比FreeBSD多了SM2】
生成密钥对并将其写入 ec_keypair.pem 文件后,我们可以检查其结构:
xxxxxxxxxx
$ openssl pkey -in ec_keypair.pem -noout –text
Private-Key: (570 bit)
priv:
… hex values …
pub:
… hex values …
ASN1 OID: sect571r1
NIST CURVE: B-571
【实操secp521r1得到的私钥是521位,书上的例子用的应该是 sect571r1
,大概率是笔误】
正如我们所看到的,EC密钥对的结构比RSA密钥对的架构更简单。这只是两个长值——私钥部分和公钥部分。
从密钥对中提取公钥的方法与我们在【第6章“非对称加密和解密”】中为RSA提取公钥的方式类似:
xxxxxxxxxx
$ openssl pkey \
-in ec_keypair.pem \
-pubout \
-out ec_public_key.pem
让我们检查一下提取的公钥的结构:
xxxxxxxxxx
$ openssl pkey -in ec_public_key.pem -pubin -noout –text
Public-Key: (570 bit)
pub:
… hex values …
ASN1 OID: sect571r1
NIST CURVE: B-571
在这里,我们可以看到公钥包含与密钥对中相同的 pub 值,并且自然不包含 priv 部分。
要进行数字签名(sign),您需要一个私钥(private key),而要验证(verify)签名,您则需要一个公钥(public key)。作为提醒,您永远不应该共享您的私钥或密钥对。你应该只分发你的公钥。
正如我们在【第6章“非对称加密和解密”】中所解释的那样,当OpenSSL文档提到私钥时,通常意味着密钥对。此外,没有命令行命令可以从密钥对中提取私有部分。这只能通过了解EC密钥在C代码中的构造方式并获取正确的数据来以编程方式完成。但是从密钥对中提取私钥在实践中没有多大意义,因为当OpenSSL需要私钥时,它可以使用整个密钥对。如果您需要使用OpenSSL在命令行上或以编程方式对某些内容进行签名,只需将整个密钥对提供给OpenSSL即可,无需费心提取私钥。
这样,我们就生成了椭圆曲线密钥对。现在,让我们签署一些东西。
OpenSSL提供了几个用于签名和验证签名的子命令。让我们来看看:
已弃用的特定于RSA的 openssl rsautl
子命令。
openssl dgst
子命令
这通常用于消息摘要计算,但也可用于对生成的摘要进行签名。这意味着它不能用于对PureEdDSA进行签名,因为该签名算法不会对摘要进行签名。
openssl pkeyutl
子命令
此子命令可用于使用OpenSSL支持的任何签名算法进行签名。在OpenSSL 3.0之前, openssl pkeyutl
不支持对长输入进行签名;用户在签名之前必须制作消息摘要。自OpenSSL 3.0以来,openssl pkeyutl
既支持文档中所称的“原始输入(raw input)”,也支持消息摘要作为输入。
我们将在示例中使用 openssl pkeyutl
子命令。其文档可以在 openssl-pkeyutl
手册页上找到:
xxxxxxxxxx
$ man openssl-pkeyutl
按照以下步骤在命令行上签名和验证签名:
首先,让我们生成一个包含要签名的示例数据的文件:
xxxxxxxxxx
$ seq 20000 >somefile.txt
现在,让我们使用SHA3-512哈希函数和我们已经生成的EC密钥对来对示例文件进行签名:
xxxxxxxxxx
$ openssl pkeyutl \
-sign \
-digest sha3-512 \
-inkey ec_keypair.pem \
-in somefile.txt \
-rawin \
-out somefile.txt.signature
让我们检查一下文件系统:
xxxxxxxxxx
$ cksum somefile.txt*
3231941463 108894 somefile.txt
2915754802 151 somefile.txt.signature
此操作的输出是写入 somefile.txt.signature 文件的151字节长的签名。
现在,如果我们共享原始的 somefile.txt 文件、somefile.txt.signature 中的签名和 ec_public_key.pem 中的公钥,任何人都可以验证签名。这是在命令行上完成的:
xxxxxxxxxx
$ openssl pkeyutl \
-verify \
-digest sha3-512 \
-inkey ec_keypair.pem \
-in somefile.txt \
-rawin \
-sigfile somefile.txt.signature
Signature Verified Successfully
请注意,我们使用 -sign
开关进行签名,使用 -verify
开关进行签名验证。
如果签名数据在传输过程中被修改了怎么办?让我们通过在签名数据后附加一些数据并再次尝试验证来模拟这种情况:
xxxxxxxxxx
$ echo "additional data" >> somefile.txt
$ openssl pkeyutl \
-verify \
-digest sha3-512 \
-inkey ec_keypair.pem \
-in somefile.txt \
-rawin \
-sigfile somefile.txt.signature
Signature Verification Failure
正如我们所看到的,在这种情况下,签名验证失败了。我们观察到,如果签名数据保持不变——即签名验证成功,但签名数据已被更改——签名验证过程会检测到它并报告失败。
现在我们已经学习了如何在命令行上对生成的签名进行数字签名和验证,让我们学习如何以编程方式进行。
OpenSSL 3.0为数字签名提供了以下API:
我们将开发一个小型的 ec-sign
程序,使用ECDSA算法对文件进行签名,类似于我们在上一节中使用 openssl pkeyutl
签名的方式。我们的程序将与 pkeyutl
互操作,这意味着 pkeyutl
将能够验证我们程序生成的签名。
以下是我们将要使用的API的一些相关手册页面:
xxxxxxxxxx
$ man PEM_read_PrivateKey
$ man PEM_read_PUBKEY
$ man EVP_DigestSignInit_ex
$ man EVP_DigestVerifyInit_ex
我们的程序将接受三个命令行参数:
我们的高层实施计划如下:
EVP_MD_CTX
对象。现在是时候通过编写必要的代码来实现我们的计划了。
让我们从实现开始:
首先,加载密钥对:
xxxxxxxxxx
const char* pkey_fname = argv[3];
FILE* pkey_file = fopen(pkey_fname, "rb");
EVP_PKEY* pkey = PEM_read_PrivateKey(
pkey_file, NULL, NULL, NULL);
然后,创建 EVP_MD_CTX
签名上下文,并用SHA3-512哈希函数和加载的密钥对初始化它:
xxxxxxxxxx
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
EVP_DigestSignInit_ex(
md_ctx,
NULL,
OSSL_DIGEST_NAME_SHA3_512,
NULL,
NULL,
pkey,
NULL);
然后,创建签名上下文初始化。让我们将输入数据提供给上下文:
xxxxxxxxxx
const char* in_fname = argv[1];
FILE* in_file = fopen(in_fname, "rb");
while (!feof(in_file)) {
size_t in_nbytes = fread(in_buf, 1, BUF_SIZE, in_file);
EVP_DigestSignUpdate(md_ctx, in_buf, in_nbytes);
}
一旦所有数据都被馈送到上下文中,我们就必须完成签名过程并获得签名。但我们不知道要为签名缓冲区分配多少内存!
幸运的是,如果我们提供NULL作为签名缓冲区指针,EVP_DigestSignFinal()
函数将为我们提供签名长度:
xxxxxxxxxx
size_t sig_len = 0;
EVP_DigestSignFinal(md_ctx, NULL, &sig_len);
请注意,EVP_DigestSignFinal()
不是唯一可以以这种方式指示所需输出缓冲区长度的OpenSSL函数。如果提供的缓冲区指针为 NULL
,许多其他OpenSSL函数也会在提供的长度指针处写入所需的缓冲区长度。
现在,我们知道签名长度,可以为签名分配内存:
xxxxxxxxxx
unsigned char* sig_buf = malloc(sig_len);
这样,我们就有了一个存储签名的地方,最终可以得到它:
xxxxxxxxxx
EVP_DigestSignFinal(md_ctx, sig_buf, &sig_len);
请注意,这一次,我们将 sig_buf
而不是 NULL
作为目标签名缓冲区提供给 EVP_DigestSignFinal()
函数。
如果我们所有要签名的数据都可以加载到一个连续的缓冲区中,我们可以使用一个名为 EVP_DigestSign()
的一次性函数,而不是使用 EVP_DigestSignInit_ex()
、EVP_DigestonUpdate()
和 EVP_DigesteSignFinal()
函数。我们不必使用ECDSA,但这将是在OpenSSL 3.0中制作PureEdDSA签名的唯一方法。
现在我们已经得到了签名,让我们将其写入输出文件:
xxxxxxxxxx
const char* sig_fname = argv[2];
sig_file = fopen(sig_fname, "wb");
fwrite(sig_buf, 1, sig_len, sig_file);
最后,让我们释放缓冲区和对象以避免内存泄漏:
xxxxxxxxxx
free(sig_buf);
free(in_buf);
EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
我们的ec-sign程序的完整源代码可以在GitHub的ec-sign.c文件中找到:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter07/ec-sign.c 。
让我们运行我们的ec-sign程序,看看它是如何工作的:
xxxxxxxxxx
$ ./ec-sign \
somefile.txt \
somefile.txt.signature \
ec_keypair.pem
Signing succeeded
据报道,该计划取得了成功。让我们使用 openssl pkeyutl
来检查我们的程序是否生成了良好的签名:
xxxxxxxxxx
$ openssl pkeyutl \
-verify \
-digest sha3-512 \
-inkey ec_keypair.pem \
-in somefile.txt \
-rawin \
-sigfile somefile.txt.signature
Signature Verified Successfully
我们的程序能够生成正确的签名,这很好。但更好的是,我们已经学会了如何编写这样的程序。我们的下一个冒险是学习如何以编程方式验证签名。
为了学习如何以编程方式验证签名,让我们开发一个小型的 ec-verify
程序,可以验证 ec-sign
或 openssl pkeyutl
生成的签名。
我们的程序将接受三个命令行参数:
以下是我们对该计划的高级实施计划:
EVP_MD_CTX
对象。现在是时候实施我们的计划了。
根据我们的计划,按照以下步骤实施 ec-verify
程序:
首先,我们必须加载签名。但是,我们不知道要为它分配多少内存。所以,让我们打开签名文件并找出它的长度:
xxxxxxxxxx
const char* sig_fname = argv[2];
FILE* sig_file = fopen(sig_fname, "rb");
fseek(sig_file, 0, SEEK_END);
long sig_file_len = ftell(sig_file);
fseek(sig_file, 0, SEEK_SET);
这种基于查找的文件长度方法可能不是最优的,但它只使用标准的C库,因此非常跨平台。
现在,当我们知道签名长度时,我们可以为签名分配内存并从签名文件中加载它:
xxxxxxxxxx
unsigned char* sig_buf = malloc(sig_file_len);
size_t sig_len = fread(sig_buf, 1, sig_file_len, sig_file);
接下来,加载验证公钥:
xxxxxxxxxx
const char* pkey_fname = argv[3];
FILE* pkey_file = fopen(pkey_fname, "rb");
EVP_PKEY* pkey = PEM_read_PUBKEY(
pkey_file, NULL, NULL, NULL);
然后,创建验证上下文,并使用SHA3-512哈希函数和加载的公钥对其进行初始化:
xxxxxxxxxx
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
EVP_DigestVerifyInit_ex(
md_ctx,
NULL,
OSSL_DIGEST_NAME_SHA3_512,
NULL,
NULL,
pkey,
NULL);
现在验证上下文已经初始化,让我们给它提供签名数据:
xxxxxxxxxx
while (!feof(in_file)) {
size_t in_nbytes = fread(in_buf, 1, BUF_SIZE, in_file);
EVP_DigestVerifyUpdate(md_ctx, in_buf, in_nbytes);
}
一旦上下文处理了所有签名的数据,我们就可以知道验证状态:
xxxxxxxxxx
int status = EVP_DigestVerifyFinal(md_ctx, sig_buf, sig_len);
如果签名验证成功,EVP_DigestVerifyFinal()
函数将返回1。任何其他返回值都表示失败。
与签名API一样,验证API也有一个名为 EVP_DigestVerify()
的一次性验证函数,该函数不逐块获取数据,但要求将整个签名数据放置在连续的内存块中。从OpenSSL 3.0开始,使用 EVP_DigestVerify()
是验证不支持流式传输的算法(如PureEdDSA)的签名的唯一方法。
一旦我们从 EVP_DigestVerifyFinal()
函数获得了验证状态,我们就不再需要使用过的缓冲区和对象,可以释放它们:
xxxxxxxxxx
free(in_buf);
EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
free(sig_buf);
我们的ec验证程序的完整源代码可以在GitHub的ec verify.c文件中找到:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter07/ec-verify.c 。
让我们运行 ec-verify
程序,并尝试验证 ec-sigh
程序生成的签名:
xxxxxxxxxx
$ ./ec-verify \
somefile.txt \
somefile.txt.signature \
ec_public_key.pem
Verification succeeded
很好——我们的 ec-verify
程序已成功验证了有效签名。
让我们尝试更改已签名的数据,并检查验证是否会失败:
xxxxxxxxxx
$ echo "additional data" >> somefile.txt
$ ./ec-verify \
somefile.txt \
somefile.txt.signature ec_public_key.pem
EVP_API error
Verification failed
正如我们所看到的,我们的 ec-verify
程序可以区分有效签名和无效签名。
本节总结了本章的实践部分。让我们继续进行总结。
在本章中,我们了解了数字签名的概念、它们如何使用消息摘要以及数字签名与消息身份验证码的区别。我们还了解了数字签名提供的加密保证。然后,我们回顾了OpenSSL支持的数字签名算法,讨论了它们的技术优点,并简要介绍了它们的历史。我们完成了理论部分,并就在何种情况下选择哪种数字签名算法提出了建议。
在实践部分,我们学习了如何使用ECDSA签名并在命令行上验证签名。然后,我们学习了如何使用C代码以编程方式对签名进行签名和验证。
在下一章中,我们将学习X.509证书和基于X.509证书的公钥基础设施(Public Key Infrastructure,PKI)。