如何在项目中应用数字签名技术

来源:互联网 发布:sql 删除表 编辑:程序博客网 时间:2024/05/01 23:52

最近公司项目中提出,内部公共WEB API接口访问需要提高安全性,最终选用token和数字签名技术来实现。那么就来记录一下其中数字签名部分的实现过程。

首先,来了解一下什么是数字签名。数字签名技术是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。数字签名是个加密的过程,数字签名验证是个解密的过程。

知道了数字签名的意义和作用,我们就可以来模拟进行数字签名实验一下了。其实数字签名技术已经很成熟,如果你也是使用java进行项目开发,那么JDK里就已经帮我们实现了数字签名和验签的方法,我们可以简便的通过调用这些方法来实现数据签名的功能,接下来我们就来实际操作一下。

首先我们确定一下,选用JDK自带的SHA1withRSA算法来进行签名和验签,由客户端(PC客户端,android客户端,IOS客户端)对发起的http请求参数进行摘要签名,服务端收到后验签,进行单向的数据签名验证,下面是我自己写的一个签名应用的例子

/**  * Project Name: UDPTest  * File Name: MySignature.java  * Package Name: signature  * Date: 2016年11月23日下午3:58:05  * Copyright (c) 2016, hadlinks All Rights Reserved.  *  */package signature;import java.io.FileInputStream;import java.io.FileOutputStream;import java.security.Key;import java.security.KeyFactory;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.KeyStore;import java.security.PrivateKey;import java.security.PublicKey;import java.security.SecureRandom;import java.security.Signature;import java.security.cert.Certificate;import java.security.spec.KeySpec;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.Arrays;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import java.util.Base64;/**  * ClassName: MySignature  * Function: RSA数字签名,数字签名遵循“私钥签名,公钥验签”原则。 * date: 2016年11月23日 下午3:58:05   *  * @author songw (songw@hadlinks.com) * @version  * @since JDK 1.8  */public class MySignature {        /**      * 数字签名算法。JDK只提供了MD2withRSA, MD5withRSA, SHA1withRSA,其他的算法需要第三方包才能支持      */    public static final String SIGNATURE_ALGORITHM = "SHA1withRSA";        public static final String ALGORITHM = "RSA";         public static final int KEYSIZE = 2048;        public static final String PUBLIC_KEY = "publicKey";        public static final String PRIVATE_KEY = "privateKey";        public static final String CHARSET = "UTF-8";        public static PublicKey publicKey = null;        public static PrivateKey privateKey = null;    public static void main(String[] args) throws Exception{        /**        * 从生成好的JKS中提取公私钥,并转化出IOS开发可用的PKCS12标准的秘钥仓库        */        try {            KeyStore keyStore = KeyStore.getInstance("JKS");            // JKS证书仓库的访问密码            char[] storePassword = "storePassword".toCharArray();            keyStore.load(new FileInputStream(                "C:\\keypair\\keypair.jks"),                storePassword);            // JKS仓库中证书/私钥的访问密码            char[] keyPassword = "keyPassword".toCharArray();            // 私钥在JKS中对应的别名            String privateKeyAlias = "test";            Key key = keyStore.getKey(privateKeyAlias, keyPassword);            KeyPair keyPair = null;            if (key instanceof PrivateKey) {                // 证书在JKS中对应的别名                String certAlias = "test";                Certificate cert = keyStore.getCertificate(certAlias);                PublicKey publicKey = cert.getPublicKey();                keyPair = new KeyPair(publicKey, (PrivateKey) key);            }            publicKey = keyPair.getPublic();            privateKey = keyPair.getPrivate();            System.out.println("生成的公钥:" + toHexString(publicKey.getEncoded()));            PublicKey a = strToPublicKey(toHexString(publicKey.getEncoded()));            System.out.println("转化后的公钥:" + toHexString(a.getEncoded()));            System.out.println("生成的私钥:" + toHexString(privateKey.getEncoded()));            PrivateKey b = strToPrivateKey(toHexString(privateKey.getEncoded()));            System.out.println("转化后的私钥:" + toHexString(b.getEncoded()));            // 将JKS转化成PKCS12标准的秘钥仓库            JSKToPKCS12("C:\\keypair\\keypair.jks",                "storePassword",                "C:\\keypair\\keypair.p12",                "newStorePasswordForP12");                System.out.println("转化P12文件成功!");            } catch (Exception ex) {                ex.printStackTrace();                return;         }            /**          * 假设现在客户端签名后向服务器端发送消息         * 用RSA的私钥进行签名         */        String[] params = {"username=username", "password=password", "token=token"};        //组成待签名的明文内容        String plainText = sortWithASCIIAsc(params);        System.out.println("待签名的明文:"+plainText);        byte[] signature = sign(privateKey, plainText.getBytes(CHARSET));        System.out.println("生成的签名:"+toHexString(signature));        //将签名得到的byte[]进行base64编码,获得sign字符串,作为get/post请求中的sign参数传给服务器端        String sign = base64Encode(signature);        System.out.println("base64编码后的签名:"+sign);                /**         * 假设现在服务器端收到了客户端的消息         * 先进行base64解码,得到客户端的signature         * 将客户端的signature内容用公钥进行验签操作         */        byte[] clientSignature = base64Decoce(sign);        System.out.println("base64解码后的签名:"+toHexString(clientSignature));                /**         * 假设服务器拦截所有来自客户端的请求         * 获取到了url参数内容         * 实际操作过程中需要重新组装、排序明文内容         * 这里为了方便直接使用处理好的明文         */        byte[] decodedText = plainText.getBytes(CHARSET);        System.out.println("验签结果" + verify(publicKey, clientSignature, decodedText));    }                /**     * 签名,三步走     * 1. 实例化,传入算法     * 2. 初始化,传入私钥     * 3. 签名     * @param key     * @param plainText     * @return     */    public static byte[] sign(PrivateKey privateKey, byte[] plainText) throws Exception{        //实例化        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);                //初始化,传入私钥        signature.initSign(privateKey);                //更新        signature.update(plainText);                //签名        return signature.sign();    }        /**     * 验签,三步走     * 1. 实例化,传入算法     * 2. 初始化,传入公钥     * 3. 验签     * @param publicKey     * @param signatureVerify     * @param plainText     * @return     */    public static boolean verify(PublicKey publicKey, byte[] signatureVerify, byte[] plainText ) throws Exception{        //实例化        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);                //初始化        signature.initVerify(publicKey);                //更新        signature.update(plainText);                //验签        return signature.verify(signatureVerify);    }            /**     * 生成RSA密钥对     * @throws Exception     */    private static Map<String, Object> generateKeyPair() throws Exception {        Map<String, Object> result = new HashMap<String, Object>();                //RSA算法要求有一个可信任的随机数源        SecureRandom secureRandom = new SecureRandom();                //为RSA算法创建一个KeyPairGenerator对象        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);                //利用上面的随机数据源初始化这个KeyPairGenerator对象        keyPairGenerator.initialize(KEYSIZE, secureRandom);                //生成密匙对        KeyPair keyPair = keyPairGenerator.generateKeyPair();                //得到公钥        PublicKey publicKey = keyPair.getPublic();                //得到私钥        PrivateKey privateKey = keyPair.getPrivate();                //填充返回结果        result.put("privateKey", privateKey);                //填充返回结果        result.put("publicKey", publicKey);                return result;    }        /**     * 利用jdk8自带的Base64工具类进行base64编码     * @param param     * @return     */    public static String base64Encode(byte[] param){    return Base64.getEncoder().encodeToString(param);    }        /**     * 利用jdk8自带的Base64工具类进行base64解码     * @param param     * @return     */    public static byte[] base64Decoce(String param){        return Base64.getDecoder().decode(param);        }        /**     * 将传入的String数组按照ASCII进行升序排列     * @param param     */    public static String sortWithASCIIAsc(String[] params){    //String本身已经实现CompareTo,所以直接使用sort方法进行排序    Arrays.sort(params);            String result = "";                //将参数组装成明文        for(String param : params){        result += param + "&";        }                return result.substring(0, result.length() - 1);    }        /**     * byte数组转字符串的方法     * @param bytes     * @return     */    public static String toHexString(byte[] bytes){        return toHexString(bytes, "");    }        public static String toHexString(byte[] bytes, String split){        if(isEmpty(bytes)){            return null;        }        StringBuilder sb = new StringBuilder();        for(byte b : bytes){            sb.append(toHexString(b)).append(split);        }        return sb.toString();    }        public static boolean isEmpty(byte[] bytes){        return bytes==null || bytes.length==0;    }        public static String toHexString(byte b){        String hex = null;        int i = (int)b;        String s = Integer.toHexString(i);        if(i >= 0){            int len = s.length();            if(len < 2){                s = "0" + s;            }            hex = s;        }        else{            hex = s.substring(6);        }        return hex.toUpperCase();    }        /**     * 还原公钥     * @param key     * @return     * @throws Exception     */    public static PublicKey strToPublicKey(String key) throws Exception{    byte[] keyBytes = parseHexStringToArray(key);    KeySpec keySpec = new X509EncodedKeySpec(keyBytes);        KeyFactory factory = KeyFactory.getInstance("RSA");        return factory.generatePublic(keySpec);    }        /**     *      * @param key     * @return     * @throws Exception     */    public static PrivateKey strToPrivateKey(String key) throws Exception{    byte[] keyBytes = parseHexStringToArray(key);    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);        KeyFactory factory = KeyFactory.getInstance("RSA");        return factory.generatePrivate(keySpec);    }        public static byte[] parseHexStringToArray(String s){        if(isEmpty(s)){            return null;        }        int len = s.length();        if(len % 2 !=0){            return null;        }        int size = len / 2;        byte[] data = new byte[size];        for(int i=0; i<size; i++){            String sub = s.substring(i*2, i*2+2);            data[i] = parseHexString(sub);        }        return data;    }        /**     * Check if a string is empty     * @param s     * @return      */    public static boolean isEmpty(String s){        return s == null || s.trim().length() == 0;    }        public static byte parseHexString(String s){        int i = Integer.parseInt(s, 16);        return (byte)i;    }            /**     * 从JKS格式转换为PKCS12格式     * @param srcFile String JKS格式证书库     * @param srcPasswd String JKS格式证书库密码     * @param destFile String PKCS12格式证书库     * @param destPasswd String PKCS12格式证书库密码     */      public static void JSKToPKCS12(String srcFile, String srcPasswd, String destFile, String destPasswd){          try {      KeyStore inputKeyStore = KeyStore.getInstance("JKS");      FileInputStream fis = new FileInputStream(srcFile);      char[] srcPwd = null, destPwd = null;      if ((srcPasswd == null) || srcPasswd.trim().equals("")) {  srcPwd = null;      } else {  srcPwd = srcPasswd.toCharArray();      }      if ((destPasswd == null) || destPasswd.trim().equals("")) {  destPwd = null;      } else {  destPwd = destPasswd.toCharArray();      }      inputKeyStore.load(fis, srcPwd);      fis.close();      KeyStore outputKeyStore = KeyStore.getInstance("PKCS12");      Enumeration<String> enums = inputKeyStore.aliases();      while (enums.hasMoreElements()) {          String keyAlias = (String) enums.nextElement();          System.out.println("alias=[" + keyAlias + "]");          outputKeyStore.load(null, destPwd );          if (inputKeyStore.isKeyEntry(keyAlias)) {      Key key = inputKeyStore.getKey(keyAlias, srcPwd);      Certificate[] certChain = inputKeyStore.getCertificateChain(keyAlias);      outputKeyStore.setKeyEntry(keyAlias, key, destPwd, certChain);          }          String fName = destFile.substring(0, destFile.indexOf(".p12"));          fName += "_" + keyAlias + ".p12";          FileOutputStream out = new FileOutputStream(fName);          outputKeyStore.store(out, destPwd);          out.close();          outputKeyStore.deleteEntry(keyAlias);          }      } catch (Exception e) {      e.printStackTrace();      }  }}
我们再来看一下签名过程中的主要步骤:

步骤一:生成RSA公私钥对。因为选用了SHA1withRSA算法,所以必然是需要一对RSA公私钥的,这里原本我们可以借助java.security包下的工具类,直接在程序中生成RSA密钥对。参考代码:

    public static final String ALGORITHM = "RSA";    public static final int KEYSIZE = 2048;    public static PublicKey publicKey = null;        public static PrivateKey privateKey = null;/**     * 生成RSA密钥对     * @throws Exception     */    private static Map<String, Object> generateKeyPair() throws Exception {        Map<String, Object> result = new HashMap<String, Object>();                //RSA算法要求有一个可信任的随机数源        SecureRandom secureRandom = new SecureRandom();                //为RSA算法创建一个KeyPairGenerator对象        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);                //利用上面的随机数据源初始化这个KeyPairGenerator对象        keyPairGenerator.initialize(KEYSIZE, secureRandom);                //生成密匙对        KeyPair keyPair = keyPairGenerator.generateKeyPair();                //得到公钥        PublicKey publicKey = keyPair.getPublic();                //得到私钥        PrivateKey privateKey = keyPair.getPrivate();                //填充返回结果        result.put("privateKey", privateKey);                //填充返回结果        result.put("publicKey", publicKey);                return result;    }

但是由于实际项目开发中存在IOS客户端,Apple是不支持直接使用字符串进行加密解密的,那么私钥的传递就成了问题。我们在程序中生成了公私钥之后,还需要一种可用的方式将私钥给到客户端。鉴于IOS开发使用的加解密工具是Openssl,所以只能借助PKCS12标准的秘钥库让IOS程序来读取私钥。这里我们借助JDK的另一个keytool工具,生成一个带有RSA密钥对的JKS标准的keystore,这样我们只需要将JKS文件转化成p12文件,就能提供给IOS开发者一个可用的RSA私钥了。

生成keystore很简单,直接在命令行里执行

keytool -genkey -alias test -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=localhost" -keypass keyPassword -storepass storePassword -keystore keypair.jks

这样就能在当前目录下获得一个JKS文件,里面包含了一对RSA密钥对

步骤二:分发密钥对。我们从JKS文件中提取出我们需要使用的公私钥,顺便将JKS转化成P12标准的秘钥库。提取到公私钥之后通过toHexString(publicKey.getEncoded())方法可以将公钥/私钥内容转化成16进制的字符串以便传递,同样的可以通过方法strToPublicKey(toHexString(publicKey.getEncoded()))重新将字符串还原成一个公钥/私钥。在本实例中,客户端使用私钥进行签名,服务器端使用公钥来验证签名。

        /**          * 假设现在客户端签名后向服务器端发送消息         * 用RSA的私钥进行签名         */        String[] params = {"username=username", "password=password", "token=token"};        //组成待签名的明文内容        String plainText = sortWithASCIIAsc(params);        System.out.println("待签名的明文:"+plainText);        byte[] signature = sign(privateKey, plainText.getBytes(CHARSET));        System.out.println("生成的签名:"+toHexString(signature));        //将签名得到的byte[]进行base64编码,获得sign字符串,作为get/post请求中的sign参数传给服务器端        String sign = base64Encode(signature);        System.out.println("base64编码后的签名:"+sign);

步骤三:生成签名字符串。有了私钥之后,我们就可以用私钥来生成签名了,假设我们客户端请求时参数为username,password和token,那么签名之前我们先要约定好使用一种排序方法,对这些参数进行排序,否则,如果是post请求,服务器端收到请求后就无法知道客户端传递的参数的顺序,那么就有可能导致验证签名的过程失败。换句话说,签名其实是对一系列有序内容的加密(加密后的内容可以看成是包含了所有参数内容的一份摘要,所以也叫摘要签名),验签就是对这部分有序的内容进行再次加密和比对,如果签名后的内容与客户端发送的摘要内容的一致,则验签通过,否则认为请求内容在传递过程中被篡改或发送方身份存疑。排序的具体方法可以由客户端和服务端自行约定,只需要保持一致即可,为了方便,直接采用ASCII码升序进行排列,对应方法sortWithASCIIAsc(params)。排序完成后,调用Signature工具类里面的sign()方法进行签名,这里要注意,签名后的字符串可能很长,为了便于传输,我们再对签名后的字符串进行base64编码,这样,签名字符串就获取成功了

/**         * 假设现在服务器端收到了客户端的消息         * 先进行base64解码,得到客户端的signature         * 将客户端的signature内容用公钥进行验签操作         */        byte[] clientSignature = base64Decoce(sign);        System.out.println("base64解码后的签名:"+toHexString(clientSignature));                /**         * 假设服务器拦截所有来自客户端的请求         * 获取到了url参数内容         * 实际操作过程中需要重新组装、排序明文内容         * 这里为了方便直接使用处理好的明文         */        byte[] decodedText = plainText.getBytes(CHARSET);        System.out.println("验签结果" + verify(publicKey, clientSignature, decodedText));
步骤四:验签。签名成功后我们还要验证一下,私钥签名出来的内容确实能用公钥来验签,所以我们按照签名的规则,模拟服务器收到http请求后,先对签名字符串进行base64解码,还原成原始签名字符串。再重新对收到的参数进行排序,然后调用Signature工具类里的verify()方法来验证签名的有效性。这样我们整个数字签名的过程就结束了。







0 0
原创粉丝点击