Java 中文官方教程 2022 版(四十二)(1)https://developer.aliyun.com/article/1488227
将签名和公钥保存在文件中
原文:
docs.oracle.com/javase/tutorial/security/apisign/step4.html
现在您已经为某些数据生成了签名,您需要将签名字节保存在一个文件中,将公钥字节保存在另一个文件中,这样您就可以通过调制解调器、软盘、邮件等方式将其发送给其他人。
- 生成签名的数据,
- 签名,
- 公钥
接收方可以通过运行您将在接下来的验证数字签名步骤中生成的VerSig
程序来验证数据是否来自您,并且在传输过程中没有被修改。该程序使用公钥来验证接收到的签名是否是接收到的数据的真实签名。
回想一下,签名是放在一个名为realSig
的字节数组中的。您可以通过以下方式将签名字节保存在名为sig
的文件中。
/* save the signature in a file */ FileOutputStream sigfos = new FileOutputStream("sig"); sigfos.write(realSig); sigfos.close();
从生成公钥和私钥步骤中回想一下,公钥是放在一个名为pub
的 PublicKey 对象中的。您可以通过调用getEncoded
方法获取编码后的密钥字节,然后将编码后的字节存储在一个文件中。您可以随意命名文件。例如,如果您的名字是 Susan,您可以将其命名为suepk
(代表"Sue 的公钥"),如下所示:
/* save the public key in a file */ byte[] key = pub.getEncoded(); FileOutputStream keyfos = new FileOutputStream("suepk"); keyfos.write(key); keyfos.close();
编译并运行程序
原文:
docs.oracle.com/javase/tutorial/security/apisign/step5.html
这里
是GenSig.java
程序的完整源代码,添加了一些注释。编译并运行它。请记住,您需要指定要签名的文件名,如
java GenSig data
您可以下载并使用名为data
的示例文件或您喜欢的任何其他文件。该文件不会被修改。它将被读取,以便为其生成签名。
执行程序后,您应该看到保存的suepk
(公钥)和sig
(签名)文件。
验证数字签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/versig.html
如果您有生成数字签名的数据,您可以验证签名的真实性。为此,您需要
- 数据
- 签名
- 用于签署数据的私钥对应的公钥
在这个例子中,您编写一个VerSig
程序来验证由GenSig
程序生成的签名。这演示了验证据称签名真实性所需的步骤。
VerSig
导入一个公钥和一个据称是指定数据文件签名的签名,然后验证签名的真实性。公钥、签名和数据文件名在命令行中指定。
创建VerSig
示例程序以导入文件并验证签名的步骤如下。
- 准备初始程序结构
创建一个名为VerSig.java
的文本文件。输入初始程序结构(导入语句、类名、main
方法等)。 - 输入并转换编码的公钥字节
从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey
。 - 输入签名字节
从指定为第二个命令行参数的文件中输入签名字节。 - 验证签名
获取一个Signature
对象并用于验证签名的公钥进行初始化。提供要验证签名的数据(来自指定为第三个命令行参数的文件),并验证签名。 - 编译和运行程序
准备初始程序结构
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep1.html
这是在本课程后续部分创建的VerSig
程序的基本结构。将此程序结构放在名为VerSig.java
的文件中。
import java.io.*; import java.security.*; import java.security.spec.*; class VerSig { public static void main(String[] args) { /* Verify a DSA signature */ if (args.length != 3) { System.out.println("Usage: VerSig " + "publickeyfile signaturefile " + "datafile"); } else try { // the rest of the code goes here } catch (Exception e) { System.err.println("Caught exception " + e.toString()); } } }
注意:
- 用于验证数据的方法位于
java.security
包中,因此程序从该包中导入所有内容。程序还从java.io
包中导入所需的用于输入要签名的文件数据的方法,以及从java.security.spec
包中导入包含X509EncodedKeySpec
类的内容。 - 期望有三个参数,分别指定公钥、签名和数据文件。
- 在本课程后续步骤中编写的代码将放在
try
和catch
块之间。
输入并转换编码的公钥字节
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep2.html
接下来,VerSig
需要从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey
。需要一个PublicKey
,因为Signature
的initVerify
方法需要它来初始化用于验证的Signature
对象。
首先,读取编码的公钥字节。
FileInputStream keyfis = new FileInputStream(args[0]); byte[] encKey = new byte[keyfis.available()]; keyfis.read(encKey); keyfis.close();
现在字节数组encKey
包含了编码的公钥字节。
你可以使用KeyFactory
类来实例化一个 DSA 公钥,从其编码中。KeyFactory
类提供了不透明密钥(类型为Key
)和密钥规范之间的转换,密钥规范是底层密钥材料的透明表示。通过不透明密钥,你可以获取算法名称、格式名称和编码的密钥字节,但不能获取密钥材料,例如,可能包括密钥本身和用于计算密钥的算法参数。 (请注意,PublicKey
,因为它扩展了Key
,本身也是一个Key
。)
所以,首先你需要一个密钥规范。假设密钥是根据 X.509 标准编码的,你可以通过以下方式获取一个,例如,如果密钥是使用 SUN 提供的内置 DSA 密钥对生成器生成的:
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);
现在你需要一个KeyFactory
对象来进行转换。该对象必须是一个可以处理 DSA 密钥的对象。
KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
最后,你可以使用KeyFactory
对象从密钥规范生成一个PublicKey
。
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
输入签名字节
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep3.html
下一步,输入作为第二个命令行参数指定的文件中的签名字节。
FileInputStream sigfis = new FileInputStream(args[1]); byte[] sigToVerify = new byte[sigfis.available()]; sigfis.read(sigToVerify); sigfis.close();
现在字节数组sigToVerify
包含了签名字节。
验证签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep4.html
您已经向VerSig
程序添加了代码
- 输入编码的密钥字节并将其转换为名为
pubKey
的PublicKey
- 将签名字节输入到名为
sigToVerify
的字节数组中
您现在可以继续进行验证。
初始化用于验证的 Signature 对象
与生成签名一样,签名是通过使用Signature
类的实例来验证的。您需要创建一个使用与生成签名相同的签名算法的Signature
对象。GenSig
程序使用的算法是来自 SUN 提供程序的 SHA1withDSA 算法。
Signature sig = Signature.getInstance("SHA1withDSA", "SUN");
接下来,您需要初始化Signature
对象。验证的初始化方法需要公钥。
sig.initVerify(pubKey);
向签名对象提供要验证的数据 现在,您需要向Signature
对象提供生成签名的数据。这些数据位于以第三个命令行参数指定的文件中。与签名时一样,逐个缓冲区读取数据,并通过调用update
方法将其提供给Signature
对象。
FileInputStream datafis = new FileInputStream(args[2]); BufferedInputStream bufin = new BufferedInputStream(datafis); byte[] buffer = new byte[1024]; int len; while (bufin.available() != 0) { len = bufin.read(buffer); sig.update(buffer, 0, len); }; bufin.close();
验证签名
一旦您向Signature
对象提供了所有数据,您可以验证该数据的数字签名并报告结果。请记住,所谓的签名已读入名为sigToVerify
的字节数组。
boolean verifies = sig.verify(sigToVerify); System.out.println("signature verifies: " + verifies);
如果所谓的签名(sigToVerify
)是由与公钥pubKey
对应的私钥生成的指定数据文件的实际签名,则verifies
值将为true
。
编译并运行程序
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep5.html
这里
是VerSig.java
程序的完整源代码,附加了一些注释。
编译并运行程序。请记住,您需要在命令行上指定三个参数:
- 包含编码的公钥字节的文件的名称
- 包含签名字节的文件的名称
- 数据文件的名称(生成签名的文件)
由于您将测试GenSig
程序的输出,您应该使用的文件名是
suepk
sig
data
这是一个示例运行;粗体表示您需要键入的内容。
%java VerSig suepk sig data signature verifies: true
弱点和替代方案
原文:
docs.oracle.com/javase/tutorial/security/apisign/enhancements.html
本课程中的GenSig
和VerSig
程序演示了使用 JDK 安全 API 生成数据的数字签名以及验证签名的用法。然而,这些程序描绘的实际场景,即发送方使用 JDK 安全 API 生成新的公钥/私钥对,发送方将编码的公钥字节存储在文件中,接收方读取密钥字节,这并不一定是现实的,并且存在一个潜在的重大缺陷。
在许多情况下,密钥不需要生成;它们已经存在,要么作为文件中的编码密钥,要么作为密钥库中的条目。
潜在的重大缺陷在于没有任何保证接收方收到的公钥的真实性,而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
中 - 密钥库类型为"JKS",这是来自 Oracle 的专有类型。
- 密钥库密码在字符数组
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(); // Save the certificate in a file named "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();
确保数据机密性
假设您希望保持数据的内容机密性,以便在传输过程中(或在您自己的计算机或磁盘上)无意或恶意尝试查看数据的人无法这样做。为了保持数据的机密性,您应该对其进行加密,仅存储和发送加密结果(称为ciphertext)。接收者可以解密密文以获得原始数据的副本。
课程:实现您自己的权限
原文:
docs.oracle.com/javase/tutorial/security/userperm/index.html
本课程演示了如何编写一个定义自己特殊权限的类。本课程的基本组件包括:
- 一个名为ExampleGame的示例游戏。
- 一个名为HighScore的类,被
ExampleGame
用来存储用户最新的高分。 - 一个名为HighScorePermission的类,用于保护对用户存储的高分值的访问。
- 用户的安全策略文件,授予
ExampleGame
更新他/她的高分的权限。
基本场景如下:
- 用户玩
ExampleGame
。 - 如果用户达到新的高分,
ExampleGame
使用HighScore
类来保存这个新值。 HighScore
类查看用户的安全策略,以检查ExampleGame
是否有权限更新用户的高分值。- 如果
ExampleGame
有权限更新高分,则 HighScore 类更新该值。
我们描述每个基本组件的关键点,然后展示一个示例运行:
- ExampleGame
- 高分类
- 高分权限类
- 一个示例策略文件
- 将所有内容整合在一起
ExampleGame
原文:
docs.oracle.com/javase/tutorial/security/userperm/game.html
下面是ExampleGame
的源代码。为简单起见,ExampleGame
实际上并不包含玩游戏的代码。它只是检索或更新用户的最高分。
要查看用户当前的最高分值,您可以运行:
java ExampleGame get
要为用户设置新的最高分值,您可以运行:
java ExampleGame set *score*
要检索用户当前的最高分,ExampleGame
只需实例化一个HighScore
对象并调用其getHighScore
方法。要为用户设置新的最高分,ExampleGame
实例化一个HighScore
对象并调用setHighScore
,将用户的新最高分传递给它。
这里是ExampleGame
的源代码,ExampleGame.java
:
package com.gamedev.games; import java.io.*; import java.security.*; import java.util.Hashtable; import com.scoredev.scores.*; public class ExampleGame { public static void main(String args[]) throws Exception { HighScore hs = new HighScore("ExampleGame"); if (args.length == 0) usage(); if (args[0].equals("set")) { hs.setHighScore(Integer.parseInt(args[1])); } else if (args[0].equals("get")) { System.out.println("score = "+ hs.getHighScore()); } else { usage(); } } public static void usage() { System.out.println("ExampleGame get"); System.out.println("ExampleGame set <score>"); System.exit(1); } }
高分类
原文:
docs.oracle.com/javase/tutorial/security/userperm/highscore.html
HighScore
类存储并保护用户在ExampleGame
(以及调用它的任何其他游戏)中的高分值的访问。为简单起见,该类将高分值保存到名为.highscore
的文件中,该文件位于用户的主目录中。但是,在允许ExampleGame
检索或更新用户的高分值之前,该类会检查用户是否已在其安全策略文件中授予ExampleGame
访问高分的权限。
检查ExampleGame
是否具有HighScorePermission
要检查ExampleGame
是否具有访问用户高分值的权限,HighScore
类必须:
- 调用
System.getSecurityManager()
以获取当前安装的安全管理器。 - 如果结果不为空(也就是说,存在一个安全管理器,而不是调用者是一个无限制的应用程序),那么
- 构造一个
HighScorePermission
对象,并 - 调用安全管理器的
checkPermission
方法,并传递新构造的HighScorePermission
对象。
这是代码:
SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission( new HighScorePermission(gameName)); }
checkPermission
方法本质上是询问安全管理器是否ExampleGame
具有指定的HighScorePermission
。换句话说,它询问安全管理器是否ExampleGame
有权限更新指定游戏(ExampleGame
)的用户高分值。底层安全框架将查阅用户的安全策略,以查看ExampleGame
是否确实具有此权限。
高分代码
这里
是HighScore
类的完整源代码。
注意:doPrivileged
方法调用用于使HighScore
能够临时访问对其可用但对调用它的代码(ExampleGame
)不可用的资源。例如,预期策略文件将授予HighScore
访问用户主目录中的.highscore
文件的权限,但不会授予这些权限给游戏,如ExampleGame
。
HighScorePermission 类
原文:
docs.oracle.com/javase/tutorial/security/userperm/perm.html
HighScorePermission
类定义了 ExampleGame
需要更新用户高分的权限。
所有权限类都应该从 java.security.Permission
或 java.security.BasicPermission
中的一个子类化。两者之间的基本区别在于,java.security.Permission
定义了需要名称和操作的更复杂的权限。例如,java.io.FilePermission
扩展自 java.security.Permission
,并需要一个名称(文件名)以及该文件允许的操作(读/写/删除)。
相比之下,java.security.BasicPermission
定义了只需要名称的更简单的权限。例如,java.lang.RuntimePermission
扩展自 java.security.BasicPermission
,只需要一个名称(如 “exitVM”),允许程序退出 Java 虚拟机。
我们的 HighScorePermission
是一个简单的权限,因此可以从 java.security.BasicPermission
扩展。
通常,BasicPermission
类中的方法实现本身不需要被其子类重写。这就是我们的 HighScorePermission
的情况,所以我们只需要实现构造函数,它们只是调用超类的构造函数,如以下
所示:
package com.scoredev.scores; import java.security.*; public final class HighScorePermission extends BasicPermission { public HighScorePermission(String name) { super(name); } // note that actions is ignored and not used, // but this constructor is still needed public HighScorePermission(String name, String actions) { super(name, actions); } }
一个示例策略文件
原文:
docs.oracle.com/javase/tutorial/security/userperm/policy.html
以下是一个完整的策略文件,供用户运行ExampleGame
使用。
这里不描述策略文件的语法;如果您感兴趣,请参阅默认策略实现和策略文件语法页面。
你不需要了解语法;你可以随时使用策略工具创建策略文件,如创建策略文件,控制应用程序的快速导览,以及签署代码并授予权限课程中所示。
以下是示例策略文件,后面是各个条目的描述。假设
- 策略文件位于 Kim 的计算机上,Kim 的密钥库命名为
kim.keystore
。 ExampleGame
已由游戏创建者 Terry 的私钥签名,相应的公钥在别名为"terry"
的密钥库条目中。HighScore
和HighScorePermissions
类是由实现它们的人(Chris)的私钥签名的,相应的公钥在别名为"chris"
的密钥库条目中。
这是策略文件:kim.policy
keystore "kim.keystore"; // Here is the permission ExampleGame needs. // It grants code signed by "terry" the // HighScorePermission, if the // HighScorePermission was signed by "chris" grant SignedBy "terry" { permission com.scoredev.scores.HighScorePermission "ExampleGame", signedBy "chris"; }; // Here is the set of permissions the HighScore // class needs: grant SignedBy "chris" { // The HighScore class needs permission to read // "user.home" to find the location of the // highscore file permission java.util.PropertyPermission "user.home", "read"; // It needs permission to read and write the // high score file itself permission java.io.FilePermission "${user.home}${/}.highscore", "read,write"; // It needs to get granted its own permission, // so it can call checkPermission // to see if its caller has permission. // Only grant it the permission // if the permission itself was signed by // "chris" permission com.scoredev.scores.HighScorePermission "*", signedBy "chris"; };
密钥库条目
密钥库是密钥和证书的存储库,用于查找策略文件中指定的签名者的公钥(在本例中为"terry"
和"chris"
)。
keytool
实用程序用于创建和管理密钥库。
对于本课程,假设 Kim 想玩ExampleGame
。如果 Kim 的密钥库命名为kim.keystore
,那么 Kim 的策略文件需要在开头加上以下行:
keystore "kim.keystore";
ExampleGame 条目
策略文件条目指定了特定代码源的一个或多个权限 - 来自特定位置(URL)的代码,或者由特定实体签名的代码,或两者兼有。
我们的策略文件需要为每个游戏添加一个条目,为该游戏的创建者签名的代码授予一个名为HighScorePermission
的权限,其名称为游戏名称。该权限允许游戏调用HighScore
方法来获取或更新该特定游戏用户的最高分值。
为ExampleGame
所需的条目是:
grant SignedBy "terry" { permission com.scoredev.scores.HighScorePermission "ExampleGame", signedBy "chris"; };
要求ExampleGame
由"terry"
签名使 Kim 知道该游戏是 Terry 开发的实际游戏。为了使其工作,Kim 必须已经将 Terry 的公钥证书存储到kim.keystore
中,别名为"terry"
。
注意,HighScorePermission
需要由实际实现该权限的"chris"
签名,以确保ExampleGame
被授予由"chris"
实现的实际权限,而不是其他人。与之前一样,为了使其工作,Kim 必须已经将 Chris 的公钥证书存储到kim.keystore
中,别名为"chris"
。
Java 中文官方教程 2022 版(四十二)(3)https://developer.aliyun.com/article/1488232