使用背景
在很多情况下,我们的软件为防止盗用,都增加了license的校验功能,通过license文件进行授权,从而让使用者不能随意使用其未购买的功能。然而,道高一尺,魔高一丈,一些用户的高手往往会找到很多控制许可的程序点,通过反编译等手段(这里针对Java语言),对控制许可的Java类进行破解,从而使license文件失效。这里就介绍一种反破解的技术,对一些关键的Class进行签名,然后在程序的另一个地方对这些class签名进行验证,如果这些class中有被改写的迹象,那么签名验证肯定会失败,签名失败后程序就不会正常运行,这样就可以进行反破解操作。
什么是数字签名
数字签名是一种防伪造技术,就是将要签名的数据(文本、或文本或二进制文件、字节数组等),使用数字摘要算法(如MD5,或更安全的SHA1),先生成数字摘要,然后用非对称加密的私钥对摘要进行加密,从而得到一个数字签名。用户要验证签名是否正确,只需要拿到公钥对签名进行解密,得到摘要,再使用数字摘要算法对原始的数据进行数字摘要计算,将计算结果和解密出来的摘要进行比较,如果相同,则说明签名正确,数据未被篡改,反之,数据就是被篡改了。
防伪为什么要使用数字签名而不是数字摘要
数字摘要是一个原始数据的散列码,结果是一小段字节数组,散列码的特点是原始数据即便有很小的差别,散列码就会有很大的不同,这样拿到散列码的人很难判断原始数据时怎么样的,其实数字摘要相当于一个原始数据的全系缩影,通过一个摘要,基本上能够唯一代表一个原始数据,人们通常通过摘要算法将原始数据进行数字摘要计算,将结果和以前生成的摘要进行比较,从而判断出数据是否被篡改,如果两个摘要一致,数据就可以认为是真实的,否则就是被篡改的。
但问题就出现了,因为摘要算法是公开的,任何人都可以篡改原始数据,然后生成摘要,这样你拿到的摘要是无法保证是真实的。这样,就需要非对称加密算法来帮忙了。
非对称加密算法(如RSA)是相对于对称加密算法而言的,对称加密算法是使用一个密钥,对数据进行加密和解密。加密者加密后的结果是要给合法的解密者的,而合法解密者必须拿到相同的密钥才能够解密,这样密钥必须公开给解密者,这样就存在一个问题,密钥在传输过程中容易被窃取,如果非法的解秘者窃取到这个密钥,那他就能够进行解密,拿到原始数据。用户也可以篡改原始数据,用密钥进行加密,将加密结果发出去,这样拿到加密信息的合法解密者就无法知道解密数据的真实性。
非对称加密就解决了这个问题,非对称加密需要两个密钥,一个是私钥,由加密者拥有,不对外发布的,也不存在密钥传输过程中被窃取的现象。另一个是公钥,是对外公布的,谁都可以拿到。非对称加密解密有两种情况,一个是私钥加密公钥解密,一个是公钥加密私钥解密。由于私钥是不公开的,即不向外传输的,所以,除了私钥拥有者,任何人都拿不到私钥,无法使用私钥来加密原始数据,这样别人就无法篡改数据,生成假的加密数据,而拿到公钥的人也能确信解密出来的数据时真实有效的。
数字签名是"摘要+非对称加密",即对摘要进行私钥加密,然后公钥解密出摘要,使用公钥解密能确保摘要的真实性,而摘要能确保原始数据的真实性。
如何对类进行签名
前面介绍了一些背景知识,下面就言归正传,说一说如何防止破解,对类进行签名的过程。
1、生成非对称加密的密钥对
首先要生成非对称加密的密钥对,这里选取了最常用的非对称加密算法RSA,这里使用Java的java.security.KeyPairGenerator来生成密钥,我们可以给它一个种子(可选),程序如下:
package demo.signature; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; public class KeyPairsGenerator { private static final char[] bcdLookup = { '0', '1', '2', '3', '4', '5','6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * 生成公钥、私钥 * * @param algorithm 算法 * @param seed 种子 * @return 返回公钥、私钥,第一个是公钥、第二个是私钥 */ public static String[] generatorKeys(String algorithm,String seed){ String[] results = new String[2]; String priKey; String pubKey; java.security.KeyPairGenerator keygen; try { keygen = java.security.KeyPairGenerator.getInstance(algorithm); SecureRandom secrand = new SecureRandom(); secrand.setSeed(seed.getBytes()); keygen.initialize(1024, secrand); KeyPair keys = keygen.genKeyPair(); PublicKey pubkey = keys.getPublic(); PrivateKey prikey = keys.getPrivate(); pubKey = bytes2Hex(pubkey.getEncoded()); priKey = bytes2Hex(prikey.getEncoded()); results[0] = pubKey; results[1] = priKey; System.out.println("生成秘钥成功"); } catch (NoSuchAlgorithmException e) { System.out.println("生成秘钥失败"); e.printStackTrace(); } return results; } /** * Transform the specified Hex String into a byte array. */ public static final byte[] hex2Bytes(String s) { byte[] bytes; bytes = new byte[s.length() / 2]; for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) Integer.parseInt(s.substring(2 * i, 2 * i + 2), 16); } return bytes; } /** * Transform the specified byte into a Hex String form. */ public static final String bytes2Hex(byte[] bcd) { StringBuffer s = new StringBuffer(bcd.length * 2); for (int i = 0; i < bcd.length; i++) { s.append(bcdLookup[(bcd[i] >>> 4) & 0x0f]); s.append(bcdLookup[bcd[i] & 0x0f]); } return s.toString(); } public static void main(String[] args) { String[] keyPairs=generatorKeys("RSA", "myKeyPairs"); System.out.println("PublicKey="+keyPairs[0]); System.out.println("PrivateKey="+keyPairs[1]); } }
运行这个程序,生成公钥PublicKey和私钥PrivateKey,结果如下:
生成秘钥成功 PublicKey=30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001 PrivateKey=30820276020100300d06092a864886f70d0101010500048202603082025c02010002818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa53020301000102818100e3019042b55102343f891faf2e193cecd093ca7e82841d28328e2e026effa6e9e26407bf60b1ce6e2c9f9253bd45b104006a199bf052168ab78e1aad156439c3d6fd3680029b7851ab1fbe524f373d3861fadd9ae7d17ba9221ffb0482ef2bae8bff4ad4b0460768e6bb7705ceacf6b6e5f64e06697fa42c283383e8a357efe1024100f4ba7f483bce6a601f8fed2874d72ec13ed4e93d128a8f38a72e088a50e0dbf57c1c3c6edc14ebbbc6e056c75d7b569813a02d216650adeed0c76d67e2f10d51024100efdada6355645bf3b86011ed380bfc2f8229a311be168ce97f7caeaa68302a353fc124cc063e0d3100796b8f60acc6e098dd80741b968ab1c7fc488824c6946302400d7265e6012b415b10c0e6c60f4d778b34b99c2b37e6972204c599c087db231ae4fbe4322f3393145944206089f969f3a73868e269edcaf0d155f3e3fe6b42510240244d4c66626339238f11434553094556ef6d5bd7f09c3b2190010ff28ca8558b0fa62cea903b4e05cf9b90f2f75fcf0de935051f0d99e04dfe05a9f8a910411f0240332ede411c2f126c492a94de8ae28007d94d298ad46ece4b09ea8e77c4268a04754e17e803d00ff217214263d6178779649b8c9509c9a5f798e19a5126b39e31
2、建立一个测试类,用于签名的类
建立一个测试用的被签名的类,如下:
package demo.signature; public class ClassToBeSigned { public static void main(String[] args) { System.out.println("I'm a class to demo digital siguature"); } }
3、对class进行签名
建立ClassSigner java文件,设置一个PRIVATE_KEY常量,将刚才生成的私钥结果写进去,然后对类路径中的Class(使用资源流读出的字节数组)进行签名,程序如下:
package demo.signature;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.security.Signature;
public class SignValidator {
public static final StringPUBLICK_KEY="30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001";
/**
* 将资源读为字符串
*
* @param resourceName
* @return
*/
private static String read2Str(String resourceName) {
InputStream in=SignValidator.class.getResourceAsStream(resourceName);
if(in==null)
return null;
else {
byte[] bytes=null;
String str=null;
try {
bytes=new byte[in.available()];
in.read(bytes);
str=new String(bytes);
return str;
} catch (Exception e) {
return null;
}finally {
try {
in.close();
} catch (Exception e) {
// ignor
}
}
}
}
/**
* 校验数字签名
*
* @param pubKeyValue 公钥
* @param data 要验证的字节数组
* @param sign 签名内容
* @return 校验结果
*/
private static boolean verifySign(String pubKeyValue,byte[] data,String sign){
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(KeyPairsGenerator.hex2Bytes(pubKeyValue));
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec);
byte[] signed = KeyPairsGenerator.hex2Bytes(sign);// 这是SignatureData输出的数字签名
Signature signetcheck = Signature.getInstance("SHA1withRSA");
signetcheck.initVerify(pubKey);
signetcheck.update(data);
if(signetcheck.verify(signed)){
return true;
}else
return false;
} catch (Throwable e) {
return false;
}
}
/**
* 验证文件的签名是否正确
*
* @param resourcePath 要验证的资源
* @param signResourceName 签名的资源名称
* @return
*/
public static boolean verify(String resourcePath,String signResourceName) {
String signature=read2Str(signResourceName);
byte[] data=ClassSigner.getBytes(resourcePath);
return verifySign(PUBLICK_KEY, data, signature);
}
}
运行这个类,得到类的签名结果:
signature=16c37a90d0cf79dd7c306ca2f419927ba64b0deb6f2bf8bbc71a24c9247327d1a1620cd317276df2a66af130357df6e5175bd03ad9681a912c294a6e38fdf622df6a3b0b2d9e34c3e360b0c0c288644ec12babb9e01a873e5e196573745b264e91a1d65e284128ad89b17081ee9bdc628993e90ed3f3b731f63b73542d80d46b
4、编写签名验证类
编写一个类SignValidator,用来验证类的签名。将签名结果放入一个文件 demo/signature/1.sig文件中,内容如下:
46f0acf113a9dbd6c7df8716d31b3b6a4dbf5b28190d726ad61c59fb82c46fbb1744e9101199c411518374b1012761859a31c79b7259d2a9d1eee65de60c1563837c4bf15459a258b1cce2653adc550233cf2eecccbee99a95b583f3f20a00698d878453686453e5953c6b61a0179c234fb2b26c89b8fd8d65bf4fc5305bc436
编写一个签名验证类,类中建立一个常量PUBLIC_KEY,将前面生成的公钥值放入用于解密,java代码如下:
package demo.signature;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.security.Signature;
public class SignValidator {
public static final StringPUBLICK_KEY="30819f300d06092a864886f70d010101050003818d0030818902818100e54b546a997d3bf73755845ae9ae7d1b2bcf5afd655e30ef21af4cb74c9062340806626c771d042389b3ff14f8163f087a0d6e485877b029271179cee4cef30be5dba77d4a4350714879772f1336ba859eebe248e4f35558b49be489d9278e4195d53536967833924db5d8fdcbf921028fe3ed62f884d657b12ca5b7cf90fa530203010001";
/**
* 将资源读为字符串
*
* @param resourceName
* @return
*/
private static String read2Str(String resourceName) {
InputStream in=SignValidator.class.getResourceAsStream(resourceName);
if(in==null)
return null;
else {
byte[] bytes=null;
String str=null;
try {
bytes=new byte[in.available()];
in.read(bytes);
str=new String(bytes);
return str;
} catch (Exception e) {
return null;
}finally {
try {
in.close();
} catch (Exception e) {
// ignor
}
}
}
}
/**
* 校验数字签名
*
* @param pubKeyValue 公钥
* @param data 要验证的字节数组
* @param sign 签名内容
* @return 校验结果
*/
private static boolean verifySign(String pubKeyValue,byte[] data,String sign){
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(KeyPairsGenerator.hex2Bytes(pubKeyValue));
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec);
byte[] signed = KeyPairsGenerator.hex2Bytes(sign);// 这是SignatureData输出的数字签名
Signature signetcheck = Signature.getInstance("SHA1withRSA");
signetcheck.initVerify(pubKey);
signetcheck.update(data);
if(signetcheck.verify(signed)){
return true;
}else
return false;
} catch (Throwable e) {
return false;
}
}
/**
* 验证文件的签名是否正确
*
* @param resourcePath 要验证的资源
* @param signResourceName 签名的资源名称
* @return
*/
public static boolean verify(String resourcePath,String signResourceName) {
String signature=read2Str(signResourceName);
byte[] data=ClassSigner.getBytes(resourcePath);
return verifySign(PUBLICK_KEY, data, signature);
}
}
6、在程序关键启动点进行签名验证
下面是一个演示的验证class签名的程序。
package demo.signature;
public class SignValidationDemo {
public static void main(String[] args) {
String resourcePath="/demo/signature/ClassToBeSigned.class";
boolean valid=SignValidator.verify(resourcePath, "1.sig");
if(!valid) {
System.out.println(resourcePath+" is not valid.");
//TODO 做相应的处理,如退出启动过程等。
}
}
}
运行这个程序,程序没有任何输出,说明签名验证正确。这时候,我们将/demo/signature/ClassToBeSigned.java修改一下,再次运行SignValidationDemo程序,发现签名验证不过,打印下面的语句:
/demo/signature/ClassToBeSigned.class is not valid.
为什么不使用jar文件签名?
我们这里讲了一个使用类路径中class签名的例子,其实为了防伪,我们也可以针对一个jar文件进行签名和验证,但对jar签名有缺点的,
jar不能被变更,如果有新需求需要在jar中增加类或修改类(不是破解),则必须重新生成签名,签名验证类要重新修改,比较麻烦。使用类签名的好处是,只对几个关键的不能修改的类进行签名,签名的类所在的jar可以增加新文件、删除和修改非关键文件,使得扩展性更好。
使用jar签名,必须要找到jar的路径,而查找路径是一个比较麻烦的事情(特别是在不同应用服务器上,jar有可能被部署在临时目录下),而使用类签名,只要在类路径中搜索就可以了,用当前类的ClassLoader搜索类资源是非常方便的。