JAR包数字签名与验证
经签名的Jar包内包含了以下内容:
- 原Jar包内的class文件和资源文件
- 签名文件 META-INF/*.SF:这是一个文本文件,包含原Jar包内的class文件和资源文件的Hash
- 签名block文件 META-INF/*.DSA:这是一个数据文件,包含签名者的 certificate 和数字签名。其中 certificate 包含了签名者的有关信息和 public key;数字签名是对 *.SF 文件内的 Hash 值使用 private key 加密得来使用 keytool 和 jarsigner 工具进行 Jar 包签名和验证
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 包进行验证。
下面使用 keytool 和 jarsigner 对它进行签名和验证
第1步:用 keytool 生成 keystore
打开CMD窗口,键入如下命令生成keystore文件,其中jamesKeyStore 为公钥秘钥数据文件,james 是alias 的 key pair,keypass 的值123456是秘钥指令,storepass 的值123456是秘钥库指令
keytool -genkey -alias james -keypass 123456 -validity 3650 -keystore jamesKeyStore -storepass 123456 |
具体生成过程见下图:
第2步:用 jarsigner 对 Jar 包进行签名
使用如下命令可以在CMD窗口中验证签名JAR包
jarsigner -verify cd-vsb-protect-control-1.0-1.jar |
2、JAVA验证JAR包签名
(1)、JDK对JAR包数字签名验证逻辑
JDK加载包文件提供了两个类JarFile和JarInputStream,两个类由如下构造方法,参数 boolean verify的作用是限制是否要生成JarVerifier对象,JarVerifier类的功能是提供验证JAR包签名的方法。
/** * Creates a new <code>JarFile</code> to read from the specified * <code>File</code> object. * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @throws IOException if an I/O error has occurred * @throws SecurityException if access to the file is denied * by the SecurityManager. */ public JarFile(File file, boolean verify) throws IOException { this(file, verify, ZipFile.OPEN_READ); } /** * Creates a new <code>JarFile</code> to read from the specified * <code>File</code> object in the specified mode. The mode argument * must be either <tt>OPEN_READ</tt> or <tt>OPEN_READ | OPEN_DELETE</tt>. * * @param file the jar file to be opened for reading * @param verify whether or not to verify the jar file if * it is signed. * @param mode the mode in which the file is to be opened * @throws IOException if an I/O error has occurred * @throws IllegalArgumentException * if the <tt>mode</tt> argument is invalid * @throws SecurityException if access to the file is denied * by the SecurityManager * @since 1.3 */ public JarFile(File file, boolean verify, int mode) throws IOException { super(file, mode); this.verify = verify; }
/** * Creates a new <code>JarInputStream</code> and reads the optional * manifest. If a manifest is present and verify is true, also attempts * to verify the signatures if the JarInputStream is signed. * * @param in the actual input stream * @param verify whether or not to verify the JarInputStream if * it is signed. * @exception IOException if an I/O error has occurred */ public JarInputStream(InputStream in, boolean verify) throws IOException { super(in); this.doVerify = verify; // This implementation assumes the META-INF/MANIFEST.MF entry // should be either the first or the second entry (when preceded // by the dir META-INF/). It skips the META-INF/ and then // "consumes" the MANIFEST.MF to initialize the Manifest object. JarEntry e = (JarEntry)super.getNextEntry(); if (e != null && e.getName().equalsIgnoreCase("META-INF/")) e = (JarEntry)super.getNextEntry(); first = checkManifest(e); } private JarEntry checkManifest(JarEntry e) throws IOException { if (e != null && JarFile.MANIFEST_NAME.equalsIgnoreCase(e.getName())) { man = new Manifest(); byte bytes[] = getBytes(new BufferedInputStream(this)); man.read(new ByteArrayInputStream(bytes)); closeEntry(); if (doVerify) { jv = new JarVerifier(bytes); mev = new ManifestEntryVerifier(man); } return (JarEntry)super.getNextEntry(); } return e; }
如下代码所示在JarInputStream类对象调用getNextEntry方法获取JarEntry对象时,如果jv对象不为空时,要调用JarVerifier类的beginEntry方法,而此方法最总调用了ManifestEntryVerifier类的mev.setEntry(null, je)方法,
ManifestEntryVerifier类用来做JAR安全证书验证。
public ZipEntry getNextEntry() throws IOException { JarEntry e; if (first == null) { e = (JarEntry)super.getNextEntry(); if (tryManifest) { e = checkManifest(e); tryManifest = false; } } else { e = first; if (first.getName().equalsIgnoreCase(JarIndex.INDEX_NAME)) tryManifest = true; first = null; } if (jv != null && e != null) { // At this point, we might have parsed all the meta-inf // entries and have nothing to verify. If we have // nothing to verify, get rid of the JarVerifier object. if (jv.nothingToVerify() == true) { jv = null; mev = null; } else { jv.beginEntry(e, mev); } } return e; }
/** * This method scans to see which entry we're parsing and * keeps various state information depending on what type of * file is being parsed. */ public void beginEntry(JarEntry je, ManifestEntryVerifier mev) throws IOException { if (je == null) return; if (debug != null) { debug.println("beginEntry "+je.getName()); } String name = je.getName(); /* * Assumptions: * 1. The manifest should be the first entry in the META-INF directory. * 2. The .SF/.DSA/.EC files follow the manifest, before any normal entries * 3. Any of the following will throw a SecurityException: * a. digest mismatch between a manifest section and * the SF section. * b. digest mismatch between the actual jar entry and the manifest */ if (parsingMeta) { String uname = name.toUpperCase(Locale.ENGLISH); if ((uname.startsWith("META-INF/") || uname.startsWith("/META-INF/"))) { if (je.isDirectory()) { mev.setEntry(null, je); return; } if (uname.equals(JarFile.MANIFEST_NAME) || uname.equals(JarIndex.INDEX_NAME)) { return; } if (SignatureFileVerifier.isBlockOrSF(uname)) { /* We parse only DSA, RSA or EC PKCS7 blocks. */ parsingBlockOrSF = true; baos.reset(); mev.setEntry(null, je); return; } // If a META-INF entry is not MF or block or SF, they should // be normal entries. According to 2 above, no more block or // SF will appear. Let's doneWithMeta. } } if (parsingMeta) { doneWithMeta(); } if (je.isDirectory()) { mev.setEntry(null, je); return; } // be liberal in what you accept. If the name starts with ./, remove // it as we internally canonicalize it with out the ./. if (name.startsWith("./")) name = name.substring(2); // be liberal in what you accept. If the name starts with /, remove // it as we internally canonicalize it with out the /. if (name.startsWith("/")) name = name.substring(1); // only set the jev object for entries that have a signature // (either verified or not) if (!name.equals(JarFile.MANIFEST_NAME)) { if (sigFileSigners.get(name) != null || verifiedSigners.get(name) != null) { mev.setEntry(name, je); return; } } // don't compute the digest for this entry mev.setEntry(null, je); return; }
(2)、使用java验证JAR包签名
看了上面JDK提供的JAR相关的工具类,我们可以使用JarInputStream类的逻辑来验证,思想是通过空读取JarEntry对象验证包文件中的每个文件数字签名是否被篡改,在获取JarInputStream类对象时设置verify参数值为true,当声明需要做签名验证时在使用jarIn.getNextJarEntry()获取JarEntry对象如果文件被篡改会跑出异常java.lang.SecurityException: SHA-256 digest error for 文件名,这个时候表明JAR签名验证不通过。
,代码实现如下:
public static void verify(String path) throws IOException{ File file = new File(path); InputStream in = new FileInputStream(file); JarInputStream jarIn = new JarInputStream(in,true); while(jarIn.getNextJarEntry() != null){ continue; } }