称加密技术的优点加密一计算量下,速度快。缺点是,加密方和解密方必须协商好秘钥,且保证秘钥安全,如果一方泄露了秘钥整个通信就会被破解,加密信息就不再安全了。

和对称加密技术只使用一个秘钥不同,非对称机密技术使用两个秘钥进行加解密,一个叫做公钥,一个叫做私钥,私钥自己来保管,公钥可以公开,使用公钥加密的数据必须使用私钥解密,反之亦然公钥和私钥是两个不同的秘钥,因为这种加密方法被称为非对称几秒技术。相比于对称加密技术,非对称加密技术安全性更好,但性能更慢。

在互联网后端技术中非对称加密技术主要用于登录、数字签名、数字证书认证等场景。

常用的非对称加密算法有:

  • RSA:RSA 是一种目前应用非常广泛、历史也比较悠久的非对称秘钥加密技术,在1977年被麻省理工学院的罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)三位科学家提出,由于难于破解,RSA 是目前应用最广泛的数字加密和签名技术,比如国内的支付宝就是通过RSA算法来进行签名验证。它的安全程度取决于秘钥的长度,目前主流可选秘钥长度为 1024位、2048位、4096位等,理论上秘钥越长越难于破解,按照维基百科上的说法,小于等于256位的秘钥,在一台个人电脑上花几个小时就能被破解,512位的秘钥和768位的秘钥也分别在1999年和2009年被成功破解,虽然目前还没有公开资料证实有人能够成功破解1024位的秘钥,但显然距离这个节点也并不遥远,所以目前业界推荐使用 2048 位或以上的秘钥,不过目前看 2048 位的秘钥已经足够安全了,支付宝的官方文档上推荐也是2048位,当然更长的秘钥更安全,但也意味着会产生更大的性能开销。

  • DSA:既 Digital Signature Algorithm,数字签名算法,他是由美国国家标准与技术研究所(NIST)与1991年提出。和 RSA 不同的是 DSA 仅能用于数字签名,不能进行数据加密解密,其安全性和RSA相当,但其性能要比RSA快。

  • ECDSA:Elliptic Curve Digital Signature Algorithm,椭圆曲线签名算法,是ECC(Elliptic curve cryptography,椭圆曲线密码学)和 DSA 的结合,椭圆曲线在密码学中的使用是在1985年由Neal Koblitz和Victor Miller分别独立提出的,相比于RSA算法,ECC 可以使用更小的秘钥,更高的效率,提供更高的安全保障,据称256位的ECC秘钥的安全性等同于3072位的RSA秘钥,和普通DSA相比,ECDSA在计算秘钥的过程中,部分因子使用了椭圆曲线算法。

数字签名在后端架构中,经常用于对url参数进行签名,防止被篡改:

下面我们举例说明三种签名算法的性能比较:

package com.github.coderxing.book.code.chapter6;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;

public class SignPerformanceTest {

    public static final int LOOP = 100;

    // 创建秘钥
    public static KeyPair createKey(String algorithm, int bit) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
        keyPairGenerator.initialize(bit);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        return keyPair;
    }

    // 验证签名
    public static boolean verify(String plainText, byte[] signature, PublicKey publicKey, String algorithm)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
        Signature publicSignature = Signature.getInstance(algorithm);
        publicSignature.initVerify(publicKey);
        publicSignature.update(plainText.getBytes("UTF-8"));
        return publicSignature.verify(signature);
    }

    // 生产签名
    public static byte[] sign(String textForSign, PrivateKey privateKey, String algorithm)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
        Signature privateSignature = Signature.getInstance(algorithm);
        privateSignature.initSign(privateKey);
        privateSignature.update(textForSign.getBytes("UTF-8"));
        return privateSignature.sign();
    }

    // 签名性能测试
    public static void testSign(String textForSign, PrivateKey privateKey, String algorithm, int bit)
            throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, UnsupportedEncodingException {
        long start = System.currentTimeMillis();

        for (int i = 0; i < LOOP; i++) {
            sign(textForSign, privateKey, algorithm);
        }

        long end = System.currentTimeMillis();
        System.out.println(algorithm + "签名耗时(" + bit + "位秘钥):" + (end - start) / ((double) LOOP) + " ms");
    }

    // 验证签名性能测试
    public static void testVerify(String plainText, byte[] signature, PublicKey publicKey, String algorithm, int bit)
            throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, UnsupportedEncodingException {
        long start = System.currentTimeMillis();

        for (int i = 0; i < LOOP; i++) {
            boolean r = verify(plainText, signature, publicKey, algorithm);
        }

        long end = System.currentTimeMillis();
        System.out.println(algorithm + "验签耗时(" + bit + "位秘钥):" + (end - start) / ((double) LOOP) + " ms");
    }

    public static String getQueryString(String url) {
        try {
            URL urlObj = new URL(url);
            return urlObj.getQuery();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static void main(String[] args)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {

        String[][] a = { { "DSA", "SHA1withDSA", "1024" }, { "DSA", "SHA256withDSA", "1024" },
                { "DSA", "SHA256withDSA", "2048" }, { "RSA", "SHA256withRSA", "1024" },
                { "RSA", "SHA256withRSA", "2048" }, { "RSA", "SHA256withRSA", "3192" },
                { "RSA", "SHA512withRSA", "1024" }, { "RSA", "SHA512withRSA", "2048" },
                { "RSA", "SHA512withRSA", "3192" }, { "RSA", "MD5withRSA", "1024" }, { "RSA", "MD5withRSA", "2048" },
                { "RSA", "MD5withRSA", "3192" }, { "EC", "SHA1withECDSA", "128" }, { "EC", "SHA1withECDSA", "256" },
                { "EC", "SHA256withECDSA", "128" }, { "EC", "SHA256withECDSA", "256" },
                { "EC", "SHA512withECDSA", "128" }, { "EC", "SHA512withECDSA", "256" },

        };

        String text = "user_id=1234&type=2&price=60";

        for (String[] eachAlg : a) {
            int bit = Integer.parseInt(eachAlg[2]);
            KeyPair keys = createKey(eachAlg[0], Integer.parseInt(eachAlg[2]));
            testSign(text, keys.getPrivate(), eachAlg[1], bit);

            byte[] sign = sign(text, keys.getPrivate(), eachAlg[1]);

            testVerify(text, sign, keys.getPublic(), eachAlg[1], bit);

            System.out.println("--------------------------------");
        }

    }

}
SHA1withDSA签名耗时(1024位秘钥):0.93 ms

## SHA1withDSA验签耗时(1024位秘钥):1.23 ms

SHA256withDSA签名耗时(1024位秘钥):0.7 ms

## SHA256withDSA验签耗时(1024位秘钥):1.03 ms

SHA256withDSA签名耗时(2048位秘钥):2.43 ms

## SHA256withDSA验签耗时(2048位秘钥):4.6 ms

SHA256withRSA签名耗时(1024位秘钥):1.08 ms

## SHA256withRSA验签耗时(1024位秘钥):0.09 ms

SHA256withRSA签名耗时(2048位秘钥):5.69 ms

## SHA256withRSA验签耗时(2048位秘钥):0.18 ms

SHA256withRSA签名耗时(3192位秘钥):19.67 ms

## SHA256withRSA验签耗时(3192位秘钥):0.38 ms

SHA512withRSA签名耗时(1024位秘钥):1.03 ms

## SHA512withRSA验签耗时(1024位秘钥):0.06 ms

SHA512withRSA签名耗时(2048位秘钥):5.71 ms

## SHA512withRSA验签耗时(2048位秘钥):0.18 ms

SHA512withRSA签名耗时(3192位秘钥):18.78 ms

## SHA512withRSA验签耗时(3192位秘钥):0.34 ms

MD5withRSA签名耗时(1024位秘钥):0.92 ms

## MD5withRSA验签耗时(1024位秘钥):0.06 ms

MD5withRSA签名耗时(2048位秘钥):5.55 ms

## MD5withRSA验签耗时(2048位秘钥):0.17 ms

MD5withRSA签名耗时(3192位秘钥):18.82 ms

## MD5withRSA验签耗时(3192位秘钥):0.36 ms

SHA1withECDSA签名耗时(128位秘钥):0.47 ms

## SHA1withECDSA验签耗时(128位秘钥):0.79 ms

SHA1withECDSA签名耗时(256位秘钥):1.04 ms

## SHA1withECDSA验签耗时(256位秘钥):1.8 ms

SHA256withECDSA签名耗时(128位秘钥):0.44 ms

## SHA256withECDSA验签耗时(128位秘钥):0.74 ms

SHA256withECDSA签名耗时(256位秘钥):1.05 ms

## SHA256withECDSA验签耗时(256位秘钥):1.84 ms

SHA512withECDSA签名耗时(128位秘钥):0.44 ms

## SHA512withECDSA验签耗时(128位秘钥):0.76 ms

SHA512withECDSA签名耗时(256位秘钥):1.02 ms

## SHA512withECDSA验签耗时(256位秘钥):1.81 ms

//注意 : Security.getAlgorithms("signature") 方法可以打印出所有当前支持的签名算法: System.out.println(Security.getAlgorithms("signature")); [NONEWITHDSA, SHA384WITHECDSA, SHA224WITHDSA, SHA256WITHRSA, MD5WITHRSA, SHA1WITHRSA, SHA512WITHRSA, MD2WITHRSA, SHA256WITHDSA, SHA1WITHECDSA, MD5ANDSHA1WITHRSA, SHA224WITHRSA, NONEWITHECDSA, SHA256WITHECDSA, SHA224WITHECDSA, SHA384WITHRSA, SHA512WITHECDSA, SHA1WITHDSA]

通过RSA对URL参数进行签名的,其流程如下图所示:

(TODO补图)

通过可信的第三方机构为用户A,用户B分配私钥和公钥,这样可以在一定程度上减低秘钥被泄露的风险。 用户A 和用户B 使用相同的算法计算明文摘要。 用户A 首先对URL参数计算摘要,再通过私钥对摘要进行加密,加密后的字符串最为签名,并把签名和原始参数一同传给用户B。 用户B 收到请求后使用和用户A相同相同的计算方法对 URL 参数计算摘要,再通过公钥对签名进行解密获得用户A计算的摘要,比对两次计算摘要值是否相同,如果相同就表示签名验证通过,否则说明数据被串改过。

  1. 通过可信的第三方机构为用户A,用户B分配私钥和公钥,这样可以在一定程度上减低秘钥被泄露的风险。
  2. 用户A 和用户B 使用相同的算法计算明文摘要。
  3. 用户A 首先对URL参数计算摘要,再通过私钥对摘要进行加密,加密后的字符串最为签名,并把签名和原始参数一同传给用户B。
  4. 用户B 收到请求后使用和用户A相同相同的计算方法对 URL 参数计算摘要,再通过公钥对签名进行解密获得用户A计算的摘要,比对两次计算摘要值是否相同,如果相同就表示签名验证通过,否则说明数据被串改过。

下面我们通过例子来说明通过 RSA 对URL加签和验签的过程,我们使用 openssl 来生成密钥对。

生成标准私钥,标准公钥内容以-----BEGIN RSA PRIVATE KEY-----开头,以-----END RSA PRIVATE KEY-----做结尾。

$ openssl genrsa -out private_key_for_rsa_sign_2048 2048
Generating RSA private key, 2048 bit long modulus
.......................................................................+++
................+++
e is 65537 (0x10001)


$ cat private_key_for_rsa_sign_2048
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAtoPueWL56XTFs3LoudDGEzT9jCTaQbl6FIuemyeYwh1xhtVS
CSOpWDXgkimBHQqG3Tg7EKjMJyN7i8mgew2/nvwsJaNxYa5N5UVlSmJw1O/JnNYI

(省略...)

HhRL+oMzaiJR4XWUQhMidjOGj1tv1JEu5/jYzng0ZLVniGYdEw6o76Lx9XpteKjr
vZmT+nNizCcvZRHTCrorp+PJaTvxwvkkRAw3t+/3r3DTBYf5Lyryyg==
-----END RSA PRIVATE KEY-----

生成公钥,公钥文件内容以-----BEGIN PUBLIC KEY-----开头,以-----END PUBLIC KEY-----结束。

$ openssl rsa -in private_key_for_rsa_sign_2048 -out public_key_for_rsa_sign_2048 -pubout
writing RSA key

$ cat public_key_for_rsa_sign_2048
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtoPueWL56XTFs3LoudDG
EzT9jCTaQbl6FIuemyeYwh1xhtVSCSOpWDXgkimBHQqG3Tg7EKjMJyN7i8mgew2/
nvwsJaNxYa5N5UVlSmJw1O/JnNYILm9yJx8VHGxu1AjJG/5VleOWmiJS7gk7HjIi
mi2r8Tv0IQ43rGz51R/rJt2kao5CTvZto3UkHdqfwg5OZMj3s4nmbIkbMGFFUO0E
W0dRvBwGsh03ig+VBQylHKKn7ckgGdeU222RsQ4m+SZkFB0N+JFmehAnkkOnEcp8
2MVpj6EONHllTZTgJVnNT9fJMsQHKkEphiK+QLU3drAKGRcY1edvUB8wAYXX6iOL
qwIDAQAB
-----END PUBLIC KEY-----

此时,原始的RSA私钥还不能直接在 Java 中使用,需要转成 PKCS#8 格式。PKCS#8 格式的秘钥被 -----BEGIN PRIVATE KEY----------END PRIVATE KEY----- 两个标记包围。

$ openssl pkcs8 -topk8 -in private_key_for_rsa_sign_2048 -out private_key_pkcs8_for_rsa_sign_2048 -nocrypt

$ cat private_key_pkcs8_for_rsa_sign_2048

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2g+55YvnpdMWz
cui50MYTNP2MJNpBuXoUi56bJ5jCHXGG1VIJI6lYNeCSKYEdCobdODsQqMwnI3uL

(省略...)

kS7n+NjOeDRktWeIZh0TDqjvovH1em14qOu9mZP6c2LMJy9lEdMKuiun48lpO/HC
+SREDDe37/evcNMFh/kvKvLK
-----END PRIVATE KEY-----

部分命令选项说明:

  • pkcs8,创建 PCKCS#8 格式的秘钥。
  • -topk8,使用传统格式的私钥来转换成 PCKCS#8 格式的秘钥。
  • -in,用于输入的秘钥文件名。
  • -out,输出的秘钥文件名。
  • -nocrypt,生成秘钥时不进行加密。

PKCS#8:PKCS(The Public-Key Cryptography Standards)是由美国RSA数据安全公司及其合作伙伴制定的一套公钥密码学标准,PKCS#8是其中的一个,它定义一种存储私钥信息的语法和格式(其具体格式定义可以访问网站 https://tools.ietf.org/html/rfc5208)。在 Java 中需要使用这种格式的私钥,但不是所有的编程语言都需要这种格式,比如PHP,.Net 就不是(待验证,支付宝官网上说的 https://doc.open.alipay.com/doc2/detail?treeId=58&articleId=103242&docType=1)。

Java 中计算签名的代码:

    /***
     * 对 URL 进行签名返回加签后的 url
     */

    public String signUrl(String url) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
            UnsupportedEncodingException, InvalidKeySpecException {

        // 生成用于计算签名的文本,通常我们使用 url 参数名和参数值得组合
        String query = getQueryString(url);
        String text = getTextForSign(parseQueryString(query));

        KeyFactory kFactory = KeyFactory.getInstance("RSA");
        byte privateKey[] = Base64.getDecoder().decode(this.currentPrivateKey);

        // java 中要使用 PKCS#8格式的私钥
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKey);
        PrivateKey publicKey = (PrivateKey) kFactory.generatePrivate(spec);

        // 计算签名
        byte[] signBytes = sign(text, publicKey);

        String signString = Base64.getEncoder().encodeToString(signBytes);

        // 吧签名拼装在URL上,作为参数传给服务端
        return url + "&" + SIGN_KEY + "=" + URLEncoder.encode(signString, "UTF-8");
    }

    /**
     * 计算摘要和签名,使用SHA256算法计算摘要,使用RSA算法和私钥加密摘要
     */
    private static byte[] sign(String textForSign, PrivateKey privateKey)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException {
        // 使用 SHA256 算法计算摘要
        Signature privateSignature = Signature.getInstance("SHA256withRSA");
        privateSignature.initSign(privateKey);
        privateSignature.update(textForSign.getBytes("UTF-8"));

        return privateSignature.sign();
    }

对 URL 进行签名的过程如下:

results matching ""

    No results matching ""