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大类加密算法,这些算法最终被有机地组合,以构建单向/双向认证服务,请读者朋友关注后续章节相关内容。