称加密技术的优点加密一计算量下,速度快。缺点是,加密方和解密方必须协商好秘钥,且保证秘钥安全,如果一方泄露了秘钥整个通信就会被破解,加密信息就不再安全了。
和对称加密技术只使用一个秘钥不同,非对称机密技术使用两个秘钥进行加解密,一个叫做公钥,一个叫做私钥,私钥自己来保管,公钥可以公开,使用公钥加密的数据必须使用私钥解密,反之亦然公钥和私钥是两个不同的秘钥,因为这种加密方法被称为非对称几秒技术。相比于对称加密技术,非对称加密技术安全性更好,但性能更慢。
在互联网后端技术中非对称加密技术主要用于登录、数字签名、数字证书认证等场景。
常用的非对称加密算法有:
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计算的摘要,比对两次计算摘要值是否相同,如果相同就表示签名验证通过,否则说明数据被串改过。
- 通过可信的第三方机构为用户A,用户B分配私钥和公钥,这样可以在一定程度上减低秘钥被泄露的风险。
- 用户A 和用户B 使用相同的算法计算明文摘要。
- 用户A 首先对URL参数计算摘要,再通过私钥对摘要进行加密,加密后的字符串最为签名,并把签名和原始参数一同传给用户B。
- 用户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 进行签名的过程如下:
- 首先抽取用来计算摘要的文本,我们这里是针对URL进行签名,主要是防止url参数被恶意篡改,所以我们使用 url 中的 query 参数来合成文本,这里我们为了举例只是做了简单的拼接,实际使用时可以使用更复杂的方式。
- 本例中使用 SHA256 算法进行摘要计算,然后通过 RSA 算法使用私钥将摘要进行加密,Java 中的 Signature 类将这两个步骤被封装成一个算法 “SHA256withRSA”。
- 最后将加密后的摘要通过 Base64 算法转化成字符串拼装到 url 参数上,比如输入 URL 为“http://coderxing.com/rsaApiTest.htm?param1=p1¶m2=p2”,经过加签之后 URL 变成了“http://coderxing.com/rsaApiTest.htm?param1=p1¶m2=p2&sign=r6Fjvy2mRbUsZuu15zNE8j6D3awar9r3SJWr7YeXCAb4EJSzMsHQjiQX4CBQQxri2p0NslFlZk8vcrvuM%2BBo4gA%2FxsX8KUK48AIkxowX7GXnWwS7D1rLksmpgCSOWv8xP6hW5dE1pZ3orN24lMojGpCUn2436%2BMt2lOHFxaSv5bUP8ERGcKwV2Abgsi2aGPH4ch5jpAPZarbx1P8%2FWTXlUjg9D%2FvcUszs3ROF3AkR%2BFaJpzYk%2Fi0K6MTYR0f1cQwVmlKFKYA2KhnfwlBqR17GcJd3hvBADyot8C%2F4tuRe%2FifDjKJOlIMzBS75SZY5AnuFYM5Fz2YDJRv8Ff7gCOxBA%3D%3D
”这种形式,其中的
sign
参数就是 url 签名。