这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言变更,了解Java SE 9及其后续版本中更新的语言功能的摘要。
请参阅JDK发行说明,了解有关所有JDK版本的新功能、增强功能以及已删除或已弃用选项的信息。
本课程中的GenSig
和VerSig
程序演示了使用JDK安全API生成数据的数字签名并验证签名的用法。然而,这些程序所描绘的实际情景并不一定真实,并且存在潜在的主要缺陷。在这些程序中,发送方使用JDK安全API生成新的公私钥对,将编码的公钥字节存储在文件中,接收方读取这些密钥字节。然而,接收方接收到的公钥的真实性没有任何保证,只有在提供给VerSig
程序的公钥本身是真实的情况下,VerSig
程序才能正确验证签名的真实性!
在许多情况下,密钥不需要生成,它们已经存在,可以是文件中的编码密钥或密钥库中的条目。
潜在的主要缺陷是接收方接收到的公钥的真实性没有任何保证,并且只有在VerSig
程序所提供的公钥本身是真实的情况下,VerSig
程序才能正确验证签名的真实性!
有时,用于签名和验证的密钥对的编码密钥字节已经存在于文件中。如果是这种情况,GenSig
程序可以导入编码的私钥字节并将其转换为签名所需的PrivateKey
,如下所示。假设包含私钥字节的文件的名称存储在privkeyfile
字符串中,字节表示已使用PKCS #8标准对DSA密钥进行编码。
FileInputStream keyfis = new FileInputStream(privkeyfile); byte[] encKey = new byte[keyfis.available()]; keyfis.read(encKey); keyfis.close(); PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encKey); KeyFactory keyFactory = KeyFactory.getInstance("DSA"); PrivateKey privKey = keyFactory.generatePrivate(privKeySpec);
GenSig
不再需要将公钥字节保存在文件中,因为它们已经存在于一个文件中。
在这种情况下,发送方向接收方发送
GenSig
导出的数据文件和签名文件。VerSig
程序保持不变,因为它已经期望文件中的编码公钥字节。
但是,如果有恶意用户拦截文件并以无法检测到的方式替换它们,那么潜在的问题如何解决?在某些情况下,这不是一个问题,因为人们已经通过面对面或通过可信任的第三方进行了公钥交换。之后,可以远程执行多次文件和签名交换(即,两个不同地点的人之间),并且可以使用公钥来验证其真实性。如果恶意用户尝试更改数据或签名,这将被VerSig
检测到。
如果无法进行面对面的密钥交换,可以尝试其他增加正确接收的可能性的方法。例如,您可以在后续的数据和签名文件交换之前,通过最安全的方法发送您的公钥,也许使用不那么安全的媒介。
总的来说,将数据和签名与公钥分开发送可以大大降低攻击的可能性。除非三个文件都被修改,并且按照下一段讨论的特定方式,VerSig
会检测到任何篡改。
如果恶意用户截获了三个文件(数据文档、公钥和签名),那个人可以将文档替换为其他内容,用私钥对其进行签名,并将替换的文档、新签名和与生成新签名的私钥相对应的公钥转发给您。然后,VerSig
将报告验证成功,您会认为文档来自原始发送者。因此,您应该采取措施确保至少公钥接收完整(VerSig
会检测其他文件的任何篡改),或者可以使用证书来简化公钥的认证,如下一节所述。
在加密学中,更常见的是交换包含公钥的证书,而不是公钥本身。
一个好处是证书由一个实体(发行者)签名,以验证所包含的公钥是另一个实体(主体或所有者)的实际公钥。通常,一个受信任的第三方证书颁发机构(CA)验证主体的身份,然后通过签署证书来保证其是所述公钥的所有者。
使用证书的另一个好处是,您可以通过使用签发者(签名者)的公钥验证数字签名,以确保接收到的证书的有效性,该公钥本身可以存储在一个证书中,该证书的签名可以使用该证书发行者的公钥进行验证;该公钥本身可以存储在一个证书中,以此类推,直到您达到一个您已经信任的公钥。
如果您无法建立信任链(可能是因为您无法获得所需的颁发者证书),则可以计算证书的指纹。每个指纹是一个相对较短的数字,可以唯一可靠地识别证书。(从技术上讲,它是使用消息摘要,也称为单向哈希函数,对证书信息进行哈希值计算的结果。)您可以联系证书所有者,比较您收到的证书的指纹与发送的指纹。如果它们相同,那么证书就是相同的。
对于GenSig
来说,创建包含公钥的证书,然后对VerSig
来说导入证书并提取公钥将更安全。然而,JDK没有公共证书API,允许您从公钥创建证书,因此GenSig
程序无法从其生成的公钥创建证书。(但是,确实有公共API可以从证书中提取公钥。)
如果你愿意,你可以使用各种安全工具而非API来对重要文件进行签名,并使用密钥库中的证书进行工作,就像在文件交换教程中所做的那样。
或者,你可以使用API来修改你的程序,使其能够使用已存在的私钥和相应的公钥(在证书中)从你的密钥库中工作。首先,修改GenSig
程序,从密钥库中提取私钥而不是生成新的密钥。首先,假设以下内容:
String
ksName
中spass
中String
alias
中kpass
中然后,你可以通过以下方式从密钥库中提取私钥。
KeyStore ks = KeyStore.getInstance("JKS"); FileInputStream ksfis = new FileInputStream(ksName); BufferedInputStream ksbufin = new BufferedInputStream(ksfis); ks.load(ksbufin, spass); PrivateKey priv = (PrivateKey) ks.getKey(alias, kpass);
你可以通过以下方式从密钥库中提取公钥证书并将其编码字节保存到名为suecert
的文件中。
java.security.cert.Certificate cert = ks.getCertificate(alias); byte[] encodedCert = cert.getEncoded(); // 将证书保存在名为 "suecert" 的文件中 FileOutputStream certfos = new FileOutputStream("suecert"); certfos.write(encodedCert); certfos.close();
然后你将数据文件、签名和证书发送给接收方。接收方通过首先使用keytool -printcert
命令获取证书的指纹来验证证书的真实性。
keytool -printcert -file suecert Owner: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US Issuer: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US Serial number: 35aaed17 Valid from: Mon Jul 13 22:31:03 PDT 1998 until: Sun Oct 11 22:31:03 PDT 1998 Certificate fingerprints: MD5: 1E:B8:04:59:86:7A:78:6B:40:AC:64:89:2C:0F:DD:13 SHA1: 1C:79:BD:26:A1:34:C0:0A:30:63:11:6A:F2:B9:67:DF:E5:8D:7B:5E
然后接收方通过调用发送方并将指纹与发送方的证书进行比较或在公共仓库中查找指纹来验证指纹的真实性。
接收方的验证程序(修改后的VerSig
)可以通过以下方式导入证书并从中提取公钥,假设证书文件名(例如,suecert
)在String
certName
中。
FileInputStream certfis = new FileInputStream(certName); java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509"); java.security.cert.Certificate cert = cf.generateCertificate(certfis); PublicKey pub = cert.getPublicKey();
假设您希望保持数据的内容机密性,以防止意外或恶意地在传输过程中(或在您自己的计算机或磁盘上)查看数据。为了保持数据的机密性,您应该对其进行加密,并仅存储和发送加密结果(称为密文)。接收方可以解密密文以获得原始数据的副本。