Jar 包签名
1,加密、摘要和数字签名
(1)公钥加密算法
关于公钥加密算法,参考维基百科词条 Public-key cryptography。
公钥加密算法又称为非对称密钥加密算法,因为它包含一个公钥-私钥对,称为key pair。即 key pair = private key + public key。
从功能上说,两个key作用相同,用一个key加密的消息,只能用另一个key解密,反之亦然。两个 key 的区别只在于谁拥有/知道它:private key 只有 key pair 的生成者知道,public key 则公开。key pair 的另一个特性是无法从一个 key 推算出另一个 key。
常用公钥加密算法是RSA和DSA。(TODO: 区别?)
(2)数字签名
关于数字签名,参考维基百科词条 Digital signature。
数字签名算法是基于公钥加密算法的,其过程是:
- 生成 key pair
- 签名:对消息进行摘要获得其Hash值;用 private key 加密消息Hash获得数字签名(Signature)
- 验证:对消息进行摘要获得其Hash值;用 public key 解开数字签名获得消息Hash;对两个Hash进行比对
由于 private key 无法伪造或从 public key 推算出,因此,消息发送者必为 private key 拥有者,由此确保了消息来源的真实性(Authentication)和不可否认性(Non-repudiation)。如果消息在发送过程中损坏或被篡改,进行摘要后Hash值必定不一致,数字签名验证无法通过,由此确保了消息内容的完备性(Integrity)。
(3)消息摘要
关于消息摘要,参考维基百科词条 Cryptographic hash function。
消息摘要算法是使用一个Hash函数对任意长度的输入数据进行处理,输出固定长度的数据。输出数据称为消息摘要。无法从消息摘要倒推出消息内容。常用的消息摘要算法是 MD5 和 SHA-1。(TODO: 区别?)
2,Jar 包签名和验证
对Jar包的数字签名和验证过程和前面描述的数字签名原理和过程一致。Jar包是待发送的消息,经过签名后,Jar包内置入了数字签名和public key,验证者可以使用这两项数据进行验证。
实际上,经签名的Jar包内包含了以下内容:
- 原Jar包内的class文件和资源文件
- 签名文件 META-INF/*.SF:这是一个文本文件,包含原Jar包内的class文件和资源文件的Hash
- 签名block文件 META-INF/*.DSA:这是一个数据文件,包含签名者的 certificate 和数字签名。其中 certificate 包含了签名者的有关信息和 public key;数字签名是对 *.SF 文件内的 Hash 值使用 private key 加密得来
(1)使用 keytool 和 jarsigner 工具进行 Jar 包签名和验证
JDK 提供了 keytool 和 jarsigner 两个工具用来进行 Jar 包签名和验证。
keytool 用来生成和管理 keystore。keystore 是一个数据文件,存储了 key pair 有关的2种数据:private key 和 certificate,而 certificate 包含了 public key。整个 keystore 用一个密码进行保护,keystore 里面的每一对 key pair 单独用一个密码进行保护。每对 key pair 用一个 alias 进行指定,alias 不区分大小写。
keytool 支持的算法是:
- 如果公钥算法为 DSA,则摘要算法使用 SHA-1。这是默认的
- 如果公钥算法为 RSA,则摘要算法采用 MD5
jarsigner 读取 keystore,为 Jar 包进行数字签名。jarsigner 也可以对签名的 Jar 包进行验证。
下面以 JDK 中的 tools.jar 包为例,使用 keytool 和 jarsigner 对它进行签名和验证。
第1步:用 keytool 生成 keystore
执行以下命令,生成文件名为 test.ks 的 keystore,并生成 alias 为 testkey 的 key pair
keytool -keystore test.ks -genkey -alias testkey
根据屏幕提示输入各项信息
输入keystore密码: 再次输入新密码: 您的名字与姓氏是什么? [Unknown]: 您的组织单位名称是什么? [Unknown]: 您的组织名称是什么? [Unknown]: 您所在的城市或区域名称是什么? [Unknown]: 您所在的州或省份名称是什么? [Unknown]: 该单位的两字母国家代码是什么 [Unknown]: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 正确吗? [否]: y 输入<testkey>的主密码 (如果和 keystore 密码相同,按回车): 再次输入新密码:
第2步:用 jarsigner 对 Jar 包进行签名
执行命令并按提示输入 keystore 和 testkey 的密码后,即可对 tools.jar 进行签名并输出为 tools_signed.jar
jarsigner -keystore test.ks -signedjar tools_signed.jar tools.jar testkey
输入密钥库的口令短语:
输入 testkey 的密钥口令:
警告:
签名者证书将在六个月内过期。
第3步:用 jarsigner 对 Jar 包进行验证
执行以下命令,验证Jar 包签名是否有效
jarsigner -verify tools_signed.jar
输出
jar 已验证。
警告:
此 jar 包含签名者证书将在六个月内过期的条目。
要了解详细信息,请使用 -verbose 和 -certs 选项重新运行。
注意,以上命令只是使用 Jar 包内的签名文件,验证 public key 与生成签名的 private key 是否是有效 key pair,以及 Jar 包内容是否完整,并没有和 keystore 进行比对。如果需要验证 Jar 包是否是使用某一 keystore 内的密钥进行的签名,可以指定如下的命令和选项:
jarsigner -verify -verbose -keystore test.ks tools_signed.j ar
输出如下,注意对每一个class或资源文件,前面状态标记中包含k,表明在 keystore 中找到了匹配的 certificate,也就是找到了匹配的 public key。如果需要打印出每一个 class文件或资源文件的 certificate 详细信息,可以增加 -certs 选项
... smk 3384 Tue Jul 19 02:02:54 CST 2011 sun/tools/attach/HotSpotAttachProvider.class smk 4597 Tue Jul 19 01:52:50 CST 2011 sun/tools/attach/HotSpotVirtualMachine.class smk 3487 Tue Jul 19 02:02:54 CST 2011 sun/tools/attach/WindowsAttachProvider.class smk 1001 Tue Jul 19 02:02:54 CST 2011 sun/tools/attach/WindowsVirtualMachine$PipedInputStream.class smk 2796 Tue Jul 19 02:02:54 CST 2011 sun/tools/attach/WindowsVirtualMachine.class 0 Tue Jul 19 01:52:50 CST 2011 sun/tools/jstack/ smk 4113 Tue Jul 19 01:52:50 CST 2011 sun/tools/jstack/JStack.class 0 Tue Jul 19 01:53:02 CST 2011 sun/tools/jinfo/ smk 4325 Tue Jul 19 01:53:02 CST 2011 sun/tools/jinfo/JInfo.class 0 Tue Jul 19 01:52:56 CST 2011 sun/tools/jmap/ smk 8177 Tue Jul 19 01:52:56 CST 2011 sun/tools/jmap/JMap.class s = 已验证签名 m = 在清单中列出条目 k = 在密钥库中至少找到了一个证书 i = 在身份作用域内至少找到了一个证书 jar 已验证。 警告: 此 jar 包含签名者证书将在六个月内过期的条目。 要了解详细信息,请使用 -verbose 和 -certs 选项重新运行。
(2)编程进行 Jar 包签名的验证
可以使用以下API在运行时对 Jar 包进行签名验证:
- java.util.jar.JarFile
- java.util.jar.JarEntry
- java.security.KeyStore
- java.security.cert.Certificate
读取 keystore 内的 certificate:
final String ksPath = ...
final String ksPass = ...
final HashMap<String, Certificate> certMap = new HashMap<String, Certificate>();
InputStream in = new FileInputStream(ksPath); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(in, ksPass.toCharArray()); Enumeration<String> aliases = ks.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); Certificate cert = s.getCertificate(alias);
certMap.put(alias, cert);
}// while
验证 Jar 包:
final String jarPath = "G:\\tmp\\jar_sign_test\\tools_signed.jar"; JarFile jar = new JarFile(jarPath, true); Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); // Verify the entry InputStream in = jar.getInputStream(entry); try { drain(in); } finally { try { in.close(); } catch (Exception e) { } } Certificate[] certs = entry.getCertificates(); if (null != certs && certs.length > 0) { for (Certificate cert : certs) { String alias = verify(cert, certMap); if (null == alias) { ... } else { ... } }// for } } // while
根据 JarEntry.getCertificates() 方法 Java doc,在调用之前,必须首先将此 JarEntry 数据完全读取,因此上面的代码段中调用了一个 drain() 方法:
private static void drain(InputStream in) throws IOException { byte[] buf = new byte[512]; while (-1 != in.read(buf)) ; }
verify() 方法用来检查 certificate 是否与 keystore 内的某个 certificate 匹配,主要用到了 Certificate.verify(PublicKey ) 方法:
private static String verify(Certificate cert, HashMap<String, Certificate> map) { Iterator<String> it = map.keySet().iterator(); while (it.hasNext()) { String alias = it.next(); try { cert.verify(map.get(alias).getPublicKey()); return alias; } catch (Exception e) { continue; } }// while return null; }