第三章:消息摘要与数字签名


一、消息摘要

1、概述

  • 消息摘要(Message Digest)又称为数字摘要(Digital Digest)
  • 它是一个唯一对应一个消息或文本的固定长度的值,它由一个单向Hash加密函数对消息进行作用而产生
  • 使用数字摘要生成的值是不可以篡改的,为了保证文件或者值的安全

2、特点

  • 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160比特位的输出
  • 只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出
  • 消息摘要是单向、不可逆的

3、常见算法

常见加密算法:

MD5

SHA1

SHA256

SHA512

百度搜索 tomcat ,进入官网下载 ,会经常发现有sha1sha512 , 这些都是数字摘要

数字摘要

在线获取消息摘要

4、获取字符串消息摘要

Demo:

public class DigestDemo1 {
    public static void main(String[] args) throws Exception{
        // 原文
        String input = "aa";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

        // 获取消息数字摘要的字节数组
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println(new String(digest));
    }
}

运行程序:

无法识别,使用 Base64 进行编码:

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import java.security.MessageDigest;

public class DigestDemo1 {
    public static void main(String[] args) throws Exception{
        // 原文
        String input = "aa";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

        // 获取消息数字摘要的字节数组
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println("消息摘要:" + new String(digest));

        //进行 base64 编码
        String encode = Base64.encode(digest);
        System.out.println("进行Base64编码:" + encode);
    }
}

再次运行:

使用在线 md5 加密 ,发现我们生成的值和代码生成的值不一样,那是因为消息摘要不是使用base64进行编码的,所以我们需要把值转成16进制

数字摘要转换成 16 进制

// 4124bc0a9335c27f086f24ba207a4912 md5 在线校验

// QSS8CpM1wn8IbyS6IHpJEg== 消息摘要使用的是16进制

转换为16进制

public class DigestDemo1 {
    public static void main(String[] args) throws Exception {
        // 原文
        String input = "aa";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

        // 获取消息数字摘要的字节数组
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println("消息摘要:" + new String(digest));

        //进行 base64 编码
        String encode = Base64.encode(digest);
        System.out.println("进行Base64编码:" + encode);

        //进行十六进制转码
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            //转成十六进制
            String s = Integer.toHexString(b & 0xff);
            if (s.length() == 1) {
                // 如果生成的字符只有一个,前面补0
                s = "0" + s;
            }
            sb.append(s);
        }
        System.out.println(sb);
    }
}

运行:

5、其他数字摘要算法

public class OtherDigestDemo2 {
    public static void main(String[] args) throws Exception{
        // 原文
        String input = "aa";
        // 算法
        String algorithm = "MD5";
        // 获取数字摘要对象
        String md5 = getDigest(input, "MD5");
        System.out.println("md5:" + md5);

        String sha1 = getDigest(input, "SHA-1");
        System.out.println("sha1:" + sha1);

        String sha256 = getDigest(input, "SHA-256");
        System.out.println("sha256:" + sha256);

        String sha512 = getDigest(input, "SHA-512");
        System.out.println("sha512:" + sha512);
    }

    /**
     * 根据摘要算法计算信息摘要
     * @param input         原文
     * @param algorithm     摘要算法
     * @return
     * @throws Exception
     */

    private static String getDigest(String input, String algorithm) throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 消息数字摘要
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println();
        System.out.println("密文的字节长度:" + digest.length);

        return toHex(digest);
    }

    /**
     *
     * @param digest
     * @return
     */

    private static String toHex(byte[] digest) {
        System.out.println("密文:" + new String(digest));

        // base64编码
        System.out.println("Base64编码:" + Base64.encode(digest));

        // 创建对象用来拼接
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            // 转成 16进制
            String s = Integer.toHexString(b & 0xff);
            if (s.length() == 1){
                // 如果生成的字符只有一个,前面补0
                s = "0"+s;
            }
            sb.append(s);
        }
        System.out.println("16进制数据的长度:" + sb.toString().getBytes().length);
        return sb.toString();
    }
}

运行程序:

6、获取文件消息摘要

案例:

public class FileDigestDemo3 {
    public static void main(String[] args) throws Exception{
        String input = "aa";
        String algorithm = "MD5";

        // sha1 可以实现秒传功能
        String sha1 = getDigestFile("apache-tomcat-9.0.10-windows-x64.zip""SHA-1");
        System.out.println(sha1);

        String sha512 = getDigestFile("apache-tomcat-9.0.10-windows-x64.zip""SHA-512");
        System.out.println(sha512);

        String md5 = getDigest("aa""MD5");
        System.out.println(md5);

        String md51 = getDigest("aa ""MD5");
        System.out.println(md51);
    }


    private static String getDigestFile(String filePath, String algorithm) throws Exception{
        FileInputStream fis = new FileInputStream(filePath);
        int len = 0;
        byte[] buffer = new byte[1024];

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        while ((len = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }

        //获取消息摘要对象
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        //获取消息摘要
        byte[] digest = messageDigest.digest(baos.toByteArray());

        System.out.println("\n密文的字节长度:" + digest.length);
        return toHex(digest);
    }


    private static String getDigest(String input, String algorithm) throws Exception{
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        byte[] digest = messageDigest.digest(input.getBytes());
        System.out.println();
        System.out.println("密文的字节长度:" + digest.length);
        return toHex(digest);
    }

    private static String toHex(byte[] digest) {
        System.out.println(new String(digest));
        // 消息摘要进行表示的时候,是用16进制进行表示
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            // 转成16进制
            String s = Integer.toHexString(b & 0xff);
            // 保持数据的完整性,前面不够的用0补齐
            if (s.length()==1){
                s="0"+s;
            }
            sb.append(s);
        }
        System.out.println("16进制数据的长度:"+ sb.toString().getBytes().length);
        return sb.toString();
    }
}

运行程序,获取文件 sha-1 和 sha-512 的值

查看 tomcat 官网上面 sha-1 和 sha-512 的值

使用 sha-1 算法,可以实现秒传功能,不管咱们如何修改文件的名字,最后得到的值是一样的

再次运行程序 ,获取 sha-1 和 sha-512 的值

如果原文修改了,那么sha-1值 就会不一样

运行结果:

7、总结

  • MD5算法:摘要结果16个字节, 转16进制后32个字节
  • SHA1算法:摘要结果20个字节, 转16进制后40个字节
  • SHA256算法:摘要结果32个字节, 转16进制后64个字节
  • SHA512算法:摘要结果64个字节, 转16进制后128个字节

二、数字签名

1、概述

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术数字摘要技术的应用。

2、简单认识

相信我们都写过信,在写信的时候落款处总是要留下自己的名字,用来表示写信的人是谁。我们签的这个字就是生活中的签名:

而数字签名呢?其实也是同样的道理,他的含义是:在网络中传输数据时候,给数据添加一个数字签名,表示是谁发的数据,而且还能证明数据没有被篡改。

OK,数字签名的主要作用就是保证了数据的有效性(验证是谁发的)和完整性(证明信息没有被篡改)。下面我们就来好好地看一下他的底层实现原理是什么样子的。

3、基本原理

为了理解得清楚,我们通过案例一步一步来讲解。话说张三有俩好哥们A、B。由于工作原因,张三和A、B写邮件的时候为了安全都需要加密。于是张三想到了数字签名:

整个思路是这个样子的:

第一步:加密采用非对称加密,张三有三把钥匙,两把公钥,送给朋友。一把私钥留给自己。

第二步:A或者B写邮件给张三:A先用公钥对邮件加密,然后张三收到邮件之后使用私钥解密。

第三步:张三写邮件给A或者B:

(1)张三写完邮件,先用hash函数生成邮件的摘要,附着在文章上面,这就完成了数字签名,然后张三再使用私钥加密。就可以把邮件发出去了。

(2)A或者是B收到邮件之后,先把数字签名取下来,然后使用自己的公钥解密即可。这时候取下来的数字签名中的摘要若和张三的一致,那就认为是张三发来的,再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。

上面的流程我们使用一张图来演示一下:

首先把公钥送给朋友A和B:

还有就是最后一个比较麻烦的,张三给A或者B发邮件:

4、数字证书

上面提到我们对签名进行验证时,需要用到公钥。如果公钥是伪造的,那我们无法验证数字签名了,也就根本不可能从数字签名确定对方的合法性了。这时候证书就闪亮登场了。我们可能都有考各种证书的经历,比如说普通话证书,四六级证书等等,但是归根结底,到任何场合我们都能拿出我们的证书来证明自己确实已经考过了普通话,考过了四六级。这里的证书也是同样的道理。

如果不理解证书的作用,我们可以举一个例子,比如说我们的毕业证书,任何公司都会承认。为什么会承认?因为那是国家发得,大家都信任国家。也就是说只要是国家的认证机构,我们都信任它是合法的。

那么这个证书是如何生成的呢?我们再来看一张图:

例如:

5、CA 认证中心

所谓CA(Certificate Authority)认证中心,它是采用PKI(Public Key Infrastructure)公开密钥基础架构技术,专门提供网络身份认证服务,CA可以是民间团体,也可以是政府机构。负责签发和管理数字证书,且具有权威性和公正性的第三方信任机构,它的作用就像我们现实生活中颁发证件的公司,如护照办理机构。国内的CA认证中心主要分为区域性CA认证中心和行业性CA认证中心。

6、网页加密

我们看一个应用“数字证书”的实例:https协议。这个协议主要用于网页加密

首先,客户端向服务器发出加密请求。

服务器用自己的私钥加密网页以后,连同本身的数字证书,一起发送给客户端。

客户端(浏览器)的“证书管理器”,有“受信任的根证书颁发机构”列表。客户端会根据这张列表,查看解开数字证书的公钥是否在列表之内。

如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。

如果这张数字证书不是由受信任的机构颁发的,浏览器会发出另一种警告。

如果数字证书是可靠的,客户端就可以使用证书中的服务器公钥,对信息进行加密,然后与服务器交换加密信息。

7、代码实现

生成签名并校验签名:

public class SignatureDemo {
    public static void main(String[] args) throws Exception {
        String input = "123";

        PublicKey publicKey = RSADemo3.getPublicKey("a.pub""RSA");
        PrivateKey privateKey = RSADemo3.getPrivateKey("a.pri""RSA");

        String signAlgorithm = "sha256withrsa";

        //生成数字签名
        String signatureData = getSignature(input, signAlgorithm, privateKey);
        System.out.println("数字签名:" + signatureData);

        //验证数字签名
        boolean verify = verifySignature(input, signAlgorithm, publicKey, signatureData);
        System.out.println("校验签名是否通过:" + verify);
    }

    /**
     * 验证签名数据
     *
     * @param input         原文
     * @param algorithm     签名算法
     * @param publicKey     公钥
     * @param signatureData 数字签名
     * @return 数据是否被篡改
     */

    private static boolean verifySignature(String input, String algorithm, PublicKey publicKey, String signatureData) throws Exception {
        // 获取签名对象
        Signature signature = Signature.getInstance(algorithm);
        // 初始化签名
        signature.initVerify(publicKey);
        // 传入原文
        signature.update(input.getBytes());
        // 校验数据
        return signature.verify(Base64.decode(signatureData));
    }

    /**
     * 生成数字签名
     *
     * @param input      原文
     * @param algorithm  加密算法
     * @param privateKey 私钥
     * @return 签名数据
     */

    private static String getSignature(String input, String algorithm, PrivateKey privateKey) throws Exception {
        // 获取签名对象
        Signature signature = Signature.getInstance(algorithm);
        // 初始化签名
        signature.initSign(privateKey);
        // 传入原文
        signature.update(input.getBytes());
        // 开始签名
        byte[] sign = signature.sign();
        // 对签名数据进行Base64编码
        return Base64.encode(sign);
    }
}

三、keytool 工具使用

1、概述

keytool工具路径:C:\Program Files\Java\jre1.8.0_91\bin

常用命令:

生成 keypair

keytool -genkeypair
keytool -genkeypair -alias lisi(后面部分是为证书指定别名,否则采用默认的名称为mykey)

看看keystore中有哪些项目:

keytool -list或keytool -list -v
keytool -exportcert -alias lisi -file lisi.cer

生成可打印的证书:

keytool -exportcert -alias lisi -file lisi.cer –rfc

显示数字证书文件中的证书信息:

keytool -printcert -file lisi.cer
直接双击lisi.cer,用window系统的内置程序打开lisi.cer

2、生成私钥公钥

(1)生成密钥证书

下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

创建一个文件夹,在该文件夹下执行如下命令行:

keytool -genkeypair -alias guigu -keyalg RSA -keypass guigu -keystore guigu.jks -storepass guigu 

Keytool 是一个java提供的证书管理工具

-alias:密钥的别名

-keyalg:使用的hash算法

-keypass:密钥的访问密码

-keystore:密钥库文件名,xc.keystore保存了生成的证书

-storepass:密钥库的访问密码

(2)查询证书信息

keytool -list -keystore guigu.jks

(3)删除别名

keytool -delete -alias guigu -keystore guigu.jsk

3、导出公钥

openssl是一个加解密工具包,这里使用openssl来导出公钥信息。

安装 openssl:http://slproweb.com/products/Win32OpenSSL.html

安装资料目录下的Win64OpenSSL-1_1_0g.exe

配置openssl的path环境变量,如下图:

本教程配置在C:\OpenSSL-Win64\bin

cmd进入guigu.jks文件所在目录执行如下命令(如下命令在windows下执行,会把-变成中文方式,请将它改成英文的-):

keytool -list -rfc --keystore guigu.jks | openssl x509 -inform pem -pubkey

下面段内容是公钥

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----

将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。