9.6 实例:带有数字签名的加密网络应用
在第8章构建DataServer应用时,我们给读者朋友留下一个问题:如何使用数字签名验证数据。这里我们不再使用消息摘要算法对数据进行摘要/验证,取而代之使用RSA签名算法对其进行签名/验证,但其实质都是通过消息摘要算法对数据进行摘要/验证。
我们继续对第8章构建的DataServer应用进行演进,使用RSA签名算法对服务器下发的数据进行签名,并在测试用例中进行验证。
首先,我们需要将第8章的RSACoder类和本章的RSACoder类进行合并。简单来讲就是将本章RSACoder类的sign()和verify()方法合并到第8章的RSACoder类。本书为了详述各个算法的具体实现将这两种RSA算法进行了分离,在实际应用中这两种算法都是一起配合使用的。
接下来,我们将使用开源组件Commons Codec的十六进制转换工具类Hex对签名进行封装/解包。相关实现如代码清单9-10所示。
代码清单9-10 RSACoder—签名封装/解包
/**
*私钥签名
*@param data待签名数据
*@param privateKey私钥
*@return String十六进制签名字符串
*@throws Exception
*/
public static String sign(byte[]data, String privateKey)throws Exception{
byte[]sign=sign(data, getKey(privateKey));
return Hex.encodeHexString(sign);
}
/**
*公钥校验
*@param data待验证数据
*@param publicKey公钥
*@param sign签名
*@return boolean成功返回true,失败返回false
*@throws Exception
*/
public static boolean verify(byte[]data, String publicKey, String sign)throws Exception{
return verify(data, getKey(publicKey),Hex.decodeHex(sign.toCharArray()));
}
完整实现如代码清单9-11所示。
代码清单9-11 RSACoder
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Hex;
/**
*RSA安全编码组件
*@author梁栋
*@version 1.0
*/
public abstract class RSACoder{
//非对称加密密钥算法
public static final String KEY_ALGORITHM="RSA";
//数字签名签名/验证算法
public static final String SIGNATURE_ALGORITHM="SHA1withRSA";
//公钥
private static final String PUBLIC_KEY="RSAPublicKey";
//私钥
private static final String PRIVATE_KEY="RSAPrivateKey";
/**
*RSA密钥长度默认1024位,密钥长度必须是64的倍数,范围在512~65536位之间。
*/
private static final int KEY_SIZE=512;
/**
*私钥解密
*@param data待解密数据
*@param key私钥
*@return byte[]解密数据
*@throws Exception
*/
public static byte[]decryptByPrivateKey(byte[]data, byte[]key)throws Exception{
//取得私钥
PKCS8EncodedKeySpec pkcs8KeySpec=new PKCS8EncodedKeySpec(key);
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
//生成私钥
PrivateKey privateKey=keyFactory.generatePrivate(pkcs8KeySpec);
//对数据解密
Cipher cipher=Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
*公钥解密
*@param data待解密数据
*@param key公钥
*@return byte[]解密数据
*@throws Exception
*/
public static byte[]decryptByPublicKey(byte[]data, byte[]key)throws Exception{
//取得公钥
X509EncodedKeySpec x509KeySpec=new X509EncodedKeySpec(key);
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
//生成公钥
PublicKey publicKey=keyFactory.generatePublic(x509KeySpec);
//对数据解密
Cipher cipher=Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
*公钥加密
*@param data待加密数据
*@param key公钥
*@return byte[]加密数据
*@throws Exception
*/
public static byte[]encryptByPublicKey(byte[]data, byte[]key)throws Exception{
//取得公钥
X509EncodedKeySpec x509KeySpec=new X509EncodedKeySpec(key);
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicKey=keyFactory.generatePublic(x509KeySpec);
//对数据加密
Cipher cipher=Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
*私钥加密
*@param data待加密数据
*@param key私钥
*@return byte[]加密数据
*@throws Exception
*/
public static byte[]encryptByPrivateKey(byte[]data, byte[]key)throws Exception{
//取得私钥
PKCS8EncodedKeySpec pkcs8KeySpec=new PKCS8EncodedKeySpec(key);
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
//生成私钥
PrivateKey privateKey=keyFactory.generatePrivate(pkcs8KeySpec);
//对数据加密
Cipher cipher=Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
*取得私钥
*@param keyMap密钥Map
*@return byte[]私钥
*@throws Exception
*/
public static byte[]getPrivateKey(Map<String, Object>keyMap)throws Exception{
Key key=(Key)keyMap.get(PRIVATE_KEY);
return key.getEncoded();
}
/**
*取得公钥
*@param keyMap密钥Map
*@return byte[]公钥
*@throws Exception
*/
public static byte[]getPublicKey(Map<String, Object>keyMap)
throws Exception{
Key key=(Key)keyMap.get(PUBLIC_KEY);
return key.getEncoded();
}
/**
*初始化密钥
*@return Map密钥Map
*@throws Exception
*/
public static Map<String, Object>initKey()throws Exception{
//实例化密钥对生成器
KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance(KEY_ALGORITHM);
//初始化密钥对生成器
keyPairGen.initialize(KEY_SIZE);
//生成密钥对
KeyPair keyPair=keyPairGen.generateKeyPair();
//公钥
RSAPublicKey publicKey=(RSAPublicKey)keyPair.getPublic();
//私钥
RSAPrivateKey privateKey=(RSAPrivateKey)keyPair.getPrivate();
//封装密钥
Map<String, Object>keyMap=new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
*签名
*@param data待签名数据
*@param privateKey私钥
*@return byte[]数字签名
*@throws Exception
*/
public static byte[]sign(byte[]data, byte[]privateKey)throws Exception{
//转换私钥材料
PKCS8EncodedKeySpec pkcs8KeySpec=new PKCS8EncodedKeySpec(privateKey);
//实例化密钥工厂
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
//取私钥对象
PrivateKey priKey=keyFactory.generatePrivate(pkcs8KeySpec);
//实例化Signature
Signature signature=Signature.getInstance(SIGNATURE_ALGORITHM);
//初始化Signature
signature.initSign(priKey);
//更新
signature.update(data);
//签名
return signature.sign();
}
/**
*公钥校验
*@param data待校验数据
*@param publicKey公钥
*@param sign数字签名
*@return boolean校验成功返回true失败返回false
*@throws Exception
*/
public static boolean verify(byte[]data, byte[]publicKey, byte[]sign)
throws Exception{
//转换公钥材料
X509EncodedKeySpec keySpec=new X509EncodedKeySpec(publicKey);
//实例化密钥工厂
KeyFactory keyFactory=KeyFactory.getInstance(KEY_ALGORITHM);
//生成公钥
PublicKey pubKey=keyFactory.generatePublic(keySpec);
//实例化Signature
Signature signature=Signature.getInstance(SIGNATURE_ALGORITHM);
//初始化Signature
signature.initVerify(pubKey);
//更新
signature.update(data);
//验证
return signature.verify(sign);
}
/**
*私钥签名
*@param data待签名数据
*@param privateKey私钥
*@return String十六进制签名字符串
*@throws Exception
*/
public static String sign(byte[]data, String privateKey)throws Exception{
byte[]sign=sign(data, getKey(privateKey));
return Hex.encodeHexString(sign);
}
/**
*公钥校验
*@param data待验证数据
*@param publicKey公钥
*@param sign签名
*@return boolean成功返回true,失败返回false
*@throws Exception
*/
public static boolean verify(byte[]data, String publicKey, String sign)
throws Exception{
return verify(data, getKey(publicKey),Hex.decodeHex(sign.toCharArray()));
}
/**
*私钥加密
*@param data待加密数据
*@param key私钥
*@return byte[]加密数据
*@throws Exception
*/
public static byte[]encryptByPrivateKey(byte[]data, String key)throws Exception{
return encryptByPrivateKey(data, getKey(key));
}
/**
*公钥加密
*@param data待加密数据
*@param key公钥
*@return byte[]加密数据
*@throws Exception
*/
public static byte[]encryptByPublicKey(byte[]data, String key)throws Exception{
return encryptByPublicKey(data, getKey(key));
}
/**
*私钥解密
*@param data待解密数据
*@param key私钥
*@return byte[]解密数据
*@throws Exception
*/
public static byte[]decryptByPrivateKey(byte[]data, String key)throws Exception{
return decryptByPrivateKey(data, getKey(key));
}
/**
*公钥解密
*@param data待解密数据
*@param key私钥
*@return byte[]解密数据
*@throws Exception
*/
public static byte[]decryptByPublicKey(byte[]data, String key)throws Exception{
return decryptByPublicKey(data, getKey(key));
}
/**
*初始化密钥
*@param keyMap密钥Map
*@return String十六进制编码密钥
*@throws Exception
*/
public static String getPrivateKeyString(Map<String, Object>keyMap)throws Exception{
return Hex.encodeHexString(getPrivateKey(keyMap));
}
/**
*初始化密钥
*@param keyMap密钥Map
*@return String十六进制编码密钥
*@throws Exception
*/
public static String getPublicKeyString(Map<String, Object>keyMap)throws Exception{
return Hex.encodeHexString(getPublicKey(keyMap));
}
/**
*获取密钥
*@param key密钥
*@return byte[]密钥
*@throws Exception
*/
public static byte[]getKey(String key)throws Exception{
return Hex.decodeHex(key.toCharArray());
}
}
接下来,我们将对DataServlet类进行改造,使其下发数据时附带该数据签名串。
我们可以使用RSACoder类的sign()方法对数据进行签名,并在控制台输出该签名。相关实现如代码清单9-12所示。
代码清单9-12 DataServlet—构建签名
//使用RSA签名算法对数据签名
String sign=RSACoder.sign(output, key);
System.err.println("sign:\r\n"+sign);
通过本章阐述,我们知道RSA签名串长度与初始化密钥长度一致。因此,我们可以将该签名串与原数据进行串连。此处,我们需要将签名串作为数据包顶部数据。最终使用AES算法将该数据包加密并回复给调用方。相关实现如代码清单9-13所示。
代码清单9-13 DataServlet—数据包处理
/*
*重新组织待输出的数据,将该数据的签名信息封装在数据包顶部
*RSA数字签名得到的签名值长度固定,与初始化密钥长度相同。
*如初始化密钥长度为512位,且使用RSAwithSHA1算法,
*则得到的签名串长度为128位十六进制串,即512位二进制数。
*/
ByteArrayOutputStream baos=new ByteArrayOutputStream();
//此处将写入128位十六进制字符
baos.write(sign.getBytes());
baos.write(output);
output=baos.toByteArray();
baos.flush();
baos.close();
//使用AES算法对数据加密并回复
HttpUtils.responseWrite(response, AESCoder.encrypt(output, k));
完整实现如代码清单9-14所示。
代码清单9-14 DataServlet
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*数据服务DataServlet
*@author梁栋
*@since 1.0
*/
public class DataServlet extends HttpServlet{
private static final long serialVersionUID=-6219906900195793155L;
//密钥
private static String key;
//Servlet初始化参数—私钥
private static final String KEY_PARAM="key";
//初始化
@Override
public void init()throws ServletException{
super.init();
//初始化密钥
key=getInitParameter(KEY_PARAM);
}
//处理POST请求
protected void doPost(HttpServletRequest request,
HttpServletResponse response)throws ServletException,
IOException{
try{
byte[]input=HttpUtils.requestRead(request);
//对秘密密钥解密
String k=new String(RSACoder.decryptByPrivateKey(input, key));
//输出秘密密钥
System.err.println(k);
//构造数据包
StringBuilder sb=new StringBuilder();
sb.append("<?xml version=\"1.0\"encoding=\"UTF-8\"?>\r\n");
sb.append("<dataGroup>\r\n");
sb.append("\t<dataItem>\r\n");
sb.append("\t\t<id>");
sb.append("10201");
sb.append("</id>\r\n");
sb.append("\t\t<price>");
sb.append("35.0");
sb.append("</price>\r\n");
sb.append("\t\t<time>");
sb.append("2009-10-30");
sb.append("</time>\r\n");
sb.append("\t</dataItem>\r\n");
sb.append("\t<dataItem>\r\n");
sb.append("\t\t<id>");
sb.append("10301");
sb.append("</id>\r\n");
sb.append("\t\t<price>");
sb.append("55.0");
sb.append("</price>\r\n");
sb.append("\t\t<time>");
sb.append("2009-10-31");
sb.append("</time>\r\n");
sb.append("\t</dataItem>\r\n");
sb.append("</dataGroup>\r\n");
byte[]output=sb.toString().getBytes();
//使用RSA签名算法对数据签名
String sign=RSACoder.sign(output, key);
System.err.println("sign:\r\n"+sign);
/*
*重新组织待输出的数据,将该数据的签名信息封装在数据包顶部
*RSA数字签名得到的签名值长度固定,与初始化密钥长度相同。
*如初始化密钥长度为512位,且使用RSAwithSHA1算法,
*则得到的签名串长度为128位十六进制串,即512位二进制数。
*/
ByteArrayOutputStream baos=new ByteArrayOutputStream();
//此处将写入128位十六进制字符
baos.write(sign.getBytes());
baos.write(output);
output=baos.toByteArray();
baos.flush();
baos.close();
//使用AES算法对数据加密并回复
HttpUtils.responseWrite(response, AESCoder.encrypt(output, k));
}catch(Exception e){
throw new ServletException(e);
}
}
}
此时,我们同样需要修改测试用例DataServletTest类相关代码,使其对服务器回应的数据进行验证操作。
这里,我们需要使用AESCoder的decrypt()方法对数据解密,并根据我们一致的签名长度对解析后的数据进行分离,得到签名及数据,并将其打印的控制台。相关实现如代码清单9-15所示。
代码清单9-15 DataServletTest—签名、数据分离
//使用AES算法对数据解密
String data=new String(AESCoder.decrypt(input, secretKey));
//分离签名、数据
String sign=data.substring(0,128);
data=data.substring(128);
System.err.println("sign:\r\n"+sign);
System.err.println("data:\r\n"+data);
接着,我们可以使用RSACoder类的verify()方法对签名进行验证。相关实现如代码清单9-16所示。
代码清单9-16 DataServletTest—签名验证
//校验
assertTrue(RSACoder.verify(data.getBytes(),publicKey, sign));
DataServletTest完整实现如代码清单9-17所示。
代码清单9-17 DataServletTest
import static org.junit.Assert.*;
import org.junit.Test;
/**
*DataServlet测试用例
*@author梁栋
*@since 1.0
*/
public class DataServletTest{
//公钥
private static final String publicKey=
"305c300d06092a864886f70d0101010500034b0030480241009fec6cff0209ef1a332a35cca
fc2aae59c4d5275ef9186d73593186482ec637f6df042c2aa41115c8a1625a2e9e9f7844c008
389e5a6379a268d877fd8edc2690203010001";
//请求地址
private static final String url=
"http://localhost:8080/dataserver/DataServlet";
@Test
public final void test()throws Exception{
//构建秘密密钥
String secretKey=AESCoder.initKeyString();
//使用RSA算法加密并发送秘密密钥
byte[]input=HttpUtils.postRequest(url, RSACoder.encryptByPublicKey
(secretKey.getBytes(),publicKey));
//使用AES算法对数据解密
String data=new String(AESCoder.decrypt(input, secretKey));
//分离签名、数据
String sign=data.substring(0,128);
data=data.substring(128);
System.err.println("sign:\r\n"+sign);
System.err.println("data:\r\n"+data);
//校验
assertTrue(RSACoder.verify(data.getBytes(),publicKey, sign));
}
}
启动DataServer服务,并执行DataServletTest类的测试方法,我们将在控制台中得到签名和数据内容,如下所示:
sign:
7f9d7535662b099f202e71b1614e7eb1ce0341e16eead56bd26e8aa0a1d4e71992f092f02
f058314 b052cb8f495823687b2effb86bf1c19f071985d5c77e8a43
data:
<?xml version="1.0"encoding="UTF-8"?>
<dataGroup>
<dataItem>
<id>10201</id>
<price>35.0</price>
<time>2009-10-30</time>
</dataItem>
<dataItem>
<id>10301</id>
<price>55.0</price>
<time>2009-10-31</time>
</dataItem>
</dataGroup>
如果验证失败,我们则认为数据在传输过程中被篡改,本次交互失败。
在实际应用中,很少会直接使用数字签名对数据进行签名/验证。并且,较少直接使用非对称加密算法对数据进行加密/解密。这些操作通常叫做底层操作,可配合数字证书、SSL/TLS协议构建单向认证或双向认证使用。
本书第6~9章详述了消息摘要算法、对称加密算法、非对称加密算法和数字签名算法共4大类加密算法,这些算法最终被有机地组合,以构建单向/双向认证服务,请读者朋友关注后续章节相关内容。