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;
}
}