最近在读Java安全框架的官方文档和BC库的官方文档,顺便做个笔记。
这个博客准备根据Java官方文档和BC库的官方文档,全面详细总结如何使用Java加解密API进行密码学编程。包括:对称非对称加密、哈希函数、消息认证码、数字签名、密钥协商、SSL安全通信、数字证书管理等Java加解密API。使用语言表述和编程实例相结合来解释如何使用Java的加解密API。
本文不会对基本的密码学算法与概念进行讲解,假设读者了解过:对称加密、非对称加密、哈希函数、数字签名、消息认证码和密钥协商算法。
但是会略为详细地叙述数字证书有关的知识点,因为这玩意挺重要的。
请按照需求阅读对应的章节—请按照需求阅读对应的章节
持续更新中,如有错误/笔误,敬请指正!
正文:
目录
- Java安全生态
- 一、组成结构
- 二、JCA和JCE核心类和接口
- 三、PKI编程手册---公钥证书的生成与管理
- 3.1 数字证书与公钥基础设施
- 3.2 数字证书核心API
- 3.2.1 基本的证书链相关类
- 3.2.2 证书链的验证类
- 3.2.3 证书链的构建类
- 3.2.4 证书/CRL的存储类
- 3.2.5 PKIX相关类
- 附录:OCSP协议支持
- 3.3、证书管理工具keytool使用手册
- 3.4 BC库文档第8章:X.509 Certificates and Attribute Certificates (重要)
- 3.5 BC库文档第9章:证书撤销和证书路径(重要)
- 3.6 BC库第12章: 证书请求和证书管理
- 四、Java安全套接字扩展 Java Secure Socket Extension (JSSE)
- 4.1 JSSE基本介绍
- 4.2 JSSE的类和接口
- 4.2.0 一个简单的代码示例
- 4.2.1 SocketFactory 和ServerSocketFactory 类
- 4.2.2 SSLSocketFactory 和 SSLServerSocketFactory 类
- 4.2.3 SSLSocket 和 SSLServerSocket 类
- 4.2.4 SSLEngine类
- 4.2.5 SSLSession类和ExtendSSLSession类
- 4.2.6 HttpsURLConnection 类
- 4.2.7 提供的类和接口
- 4.2.8 辅助支持类和接口
- 4.3 定制JSSE
- 4.4 代码示例
- 五、Java通用安全服务 Java Generic Security Services (Java GSS-API)
- 六、Java认证和授权服务 Java Authentication and Authorization Service (JAAS)
- 七、椭圆曲线
Java安全生态
文章内容说明:
本博客是我学习官方文档《security-developer-guide》和BC库文档《Java Cryptography Tools and Techniques》的笔记。本博客各章内容说明如下
- 第一章:Java安全框架的宏观架构,是我精简后整理的,内容简单但是很重要,这玩意影响你对Java安全框架的理解。如想深入了解,参见官方文档前两章。
- 第二章:本章内容是Java对各种密码学原语的实现,是对官方文档的第二章的精简,还有Java Cryptography Tools and Techniques种的一些内容。学完这章你就可以实现各种基础的密码学算法。
- 第三章:本章内容基本是对官方文档第九章的翻译。此外还额外添加了一些内容用于解释
PKI
和数字证书是个什么东西。 - 第四章:本章内容基本是对官方文档的第八章的翻译。
- 第五章:
- 第六章:
也推荐看下面这本电子书,这使用大量的例子讲解Java的加解密API:
Beginning Cryptography With Java
一、组成结构
Java安全框架分为4各部分:
- JCA(Java Cryptography Architecture Java加解密体系结构)。该部分提供基本的加密框架,如证书、数字签名、消息摘要和密钥对生成器。
- JCE(Java Cryptography Extension Java加解密扩展包) 这部分在JCA的基础上做了一些扩展,包括提供各种加密算法、消息摘要算法和密钥管理功能等。
- JSSE(Java Secure Sockets Extension Java安全套接字扩展包)。本包提供基于SSL的加密功能。
- JAAS(Java Authentication and Authorization Service鉴别与认证服务)。本包提供在Java平台上进行用户鉴别的功能。
JCA和JCE是Java平台提供的用于安全和加密服务的两组API,是一系列接口和抽象类的集合。他们不提供任何算法的实现,只为各种密码服务提供标准接口(引擎)。可以通过实现这些接口来实现对应的功能。这些功能实现后可以打包成一个Provider(安全提供者,对应Provider抽象类的一个实现类),然后加载到Java运行时环境中。
安全提供者是承担特定安全机制实现的第三方。其中Bouncy Castle是一个比较完善且免费的安全提供者。
根据美国出口限制规定,JCA可出口,但是JCE对部分国家限制出口。所以如需要完整的安全服务,就需要第三方的安全提供者,如Bouncy Castle JCE。
安全提供者、引擎和算法(重要)
https://www.zybuluo.com/changedi/note/423884#%E5%BC%95%E6%93%8E
安全提供者(Cryptographic Service Providers CSP)实现了两个概念的抽象:引擎和算法。引擎是一种密码学原语的抽象表示。如消息摘要(即哈希函数)、对称加密、非对称加密、数字签名等。引擎是一类算法的统称。而每种引擎(每一类密码学算法)都有多种不同的实际算法,如哈希函数包括MD5、SHA-256等。引擎是抽象的概念、算法是具体的算法。而每一种算法(如MD5)又可以由不同的提供者实现。
安全提供者的目的就是提供一个简单的机制,使得可以很方便地改变或者替换算法及其实现。在开发中,只需面向引擎编程即可,而不需要关心实际进行运算的类是哪一个。即在使用Java的密码学服务时,只需要指定我们需要哪种密码服务(如需要MD5),而不需要关心是哪个提供者实现的或者是怎么实现的。
-
引擎
JVM提供引擎类,是Java核心API的一部分,是一种密码学功能的顶层抽象 。如消息摘要算法(MessageDigest)。每一个引擎类都有
getInstance()
方法。这个方法会顺序查找已安装的Provider,以返回一个具体的算法实现。 -
算法
针对每一种引擎,都会有一组算法实现,如消息摘要算法的各种具体算法(MD5,SHA-256)。Java提供了一组默认的算法实现(由Sun提供),第三方的机构可以提供其他实现。
-
提供者(Provider)
算法类是由提供者来管理的,提供者知道如何将算法与实现的具体类对应起来。对应一个Provider类。
-
安全类(Security)
安全类保存提供者列表,可以通过安全类查看有哪些提供者,以及它们提供的算法支持有哪些。
安全提供者体系的一个流程如下:
业务类调用引擎类的getInstance
方法并传入算法名称,然后安全类会根据参数到他管理的Provider中顺序查找实现这个算法的Provider,并由这个Provider返回它的实现类的实例,也可以在调用getInstance
方法时指定Provider。
getInstance(String algorithm)
getInstance(String algorithm, String provider)
getInstance(String algorithm,Provider provider)
二、JCA和JCE核心类和接口
2.1 Provider 类
Provider类是密码服务提供者程序包的入口,他里面存储了这个密码服务提供者包的相关信息,包括提供者的名称、版本号和实现的密码算法等。
JDK1.8默认安装的提供者:
SUN version 1.8
SunRsaSign version 1.8
SunEC version 1.8
SunJSSE version 1.8
SunJCE version 1.8
SunJGSS version 1.8
SunSASL version 1.8
XMLDSig version 1.8
SunPCSC version 1.8
SunMSCAPI version 1.8
Provider类提供了一个getServices()
方法来获取提供者实现的所有密码服务。
2.2 Security 类
Security类用来管理已经安装的提供者和一些安全相关的参数。它可以用来添加、删除提供者。
添加提供者(以添加BC库为例)
Security.addProvider(new BouncyCastleProvider());
获取已安装的提供者
Provider[] providers = Security.getProviders();
等。
2.3 随机数生成
SecureRandom类
他是一个引擎类,由提供者实现。
//getInstance方法指定算法名
SecureRandom instance = SecureRandom.getInstance("windows-prng");
//使用默认的算法。底层还是调用getInstance方法
SecureRandom secureRandom = new SecureRandom();
设置种子
如果在获取SecureRandom类时没有提供随机种子,那么它将使用一个随机的种子。也可以给他传入种子(种子的随机性直接影响生成器生成的数的随机性):
//实例化时以一个字节数组作为种子
SecureRandom secureRandom1 = new SecureRandom("seed".getBytes());
//实例化后设置种子
secureRandom1.setSeed("seed".getBytes());
2.4 密钥相关
2.4.1 密钥的不透明表示
这种密钥的表示方法不允许程序直接获得里面密钥的各种参数信息,只提供少量的方法来获得密钥的信息。密钥不透明表示的顶层接口为
Key
。
Key接口
Key
接口是所有密钥的不透明表示的顶层接口,它定义了三个访问密钥信息的方法:getAlgorithm
、getFormat
、getEncoded
,他们分别可以用来获取密钥对应的算法名称、密钥的编码格式和密钥编码后的字节表示。编码后的字节表示可以用来进行网络传输或本地存储。
Key接口的三大子接口
- SecretKey 对称密钥
- PublicKey 公钥
- PrivateKey 私钥
这三个接口不扩展任何方法,他们只用来对密钥进行分类和提供类型安全。
KeyPair类
它只用来封装公私钥对,没有什么特殊的方法。
各种具体的密钥不透明表示子接口
• SecretKey
– PBEKey
• PrivateKey
– DHPrivateKey
– DSAPrivateKey
– ECPrivateKey
– RSAMultiPrimePrivateCrtKey
– RSAPrivateCrtKey
– RSAPrivateKey
• PublicKey
– DHPublicKey
– DSAPublicKey
– ECPublicKey
– RSAPublicKey
他们为不同的特殊的算法提供编程规范、具体由服务提供者实现。
2.4.2 密钥的透明表示
与密钥的不透明表示相比,密钥的透明表示提供了许多get
方法来访问密钥材料中的各种参数信息。
KeySpce接口
这个接口不提供任何方法,它只用来分组和提供类型规范
KeySpce的各种实现类
• SecretKeySpec
• EncodedKeySpec //抽象类,公钥或私钥的编码表示。具体的子类代表使用的具体的编码规范
– PKCS8EncodedKeySpec
– X509EncodedKeySpec
• DESKeySpec
• DESedeKeySpec
• PBEKeySpec
• DHPrivateKeySpec
• DSAPrivateKeySpec
• ECPrivateKeySpec
• RSAPrivateKeySpec
– RSAMultiPrimePrivateCrtKeySpec
– RSAPrivateCrtKeySpec
• DHPublicKeySpec
• DSAPublicKeySpec
• ECPublicKeySpec
• RSAPublicKeySpec
密钥的透明表示可以提供了使用密钥编码作为参数的构造器,可以使用不透明密钥的编码表示(getEncoded方法返回值)作为参数来实例化一个特定于算法的密钥透明表示:
SecretKey key;
byte[] keyEncoded=key.getEncoded();
SecretKeySpec bobAesKey = new SecretKeySpec(keyEncoded,"AES");
2.4.3 密钥生成器和密钥工厂
密钥生成器
密钥生成器是为各种需要密钥的算法生成可用密钥的引擎类,包括对称密钥和非对称密钥。在生成不同的算法的密钥时,通常需要提供相应的参数,生成算法再根据提供的参数生成密钥。如果不提供对应的参数,生成器则使用默认的参数。
密钥对生成器:KeyPairGenerator
这个类是用于生成公私钥对的引擎类,可以调用它的getInstance
方法来获得一个适用于指定算法的公私钥对。如果不初始化则使用默认参数。
//获取实例,不指定Provider
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
//初始化,指定密钥大小为2048比特,这个方法使用默认的随机源
rsa.initialize(2048);
//初始化,指定密钥大小为2048比特,这个方法使用指定的随机源
//rsa.initialize(2048,new SecureRandom("seed".getBytes()));
//生成密钥对
KeyPair keyPair = rsa.generateKeyPair();
密钥生成器:KeyGenerator
这个类是用于生成对称密钥的引擎类,可以调用它的getInstance
方法来获得一个适用于指定算法的对称密钥。
//获取实例
KeyGenerator aes = KeyGenerator.getInstance("AES");
//初始化
aes.init(128);
//生成密钥
SecretKey key = aes.generateKey();
注意:这里展示的只有与算法无关方式的初始化方法:通过init算法或initialize算法单独传入需要设置的各个参数。这两个初始化方法还有一种重载的方法,它以算法相关的方式进行初始化,即传入AlgorithmParameterSpec接口的一个实现类的实例。这个接口类型在后面会讲到,他是一个算法的参数的透明表示,里面承载了特定于算法的各种参数。
密钥工厂
非对称密钥工厂:KeyFactory
它是一个引擎类,可以使用getInstamce
方法来获得加工指定类型密钥的工厂实例。它是用来双向转换非对称密钥的透明和不透明表示。
示例:
//首先拿到一个公钥
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
rsa.initialize(1024);
KeyPair keyPair = rsa.generateKeyPair();
PublicKey aPublic = keyPair.getPublic();
//获得一个密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//不透明密钥转透明密钥
RSAPublicKeySpec keySpec = keyFactory.getKeySpec(aPublic, RSAPublicKeySpec.class);
//透明密钥转不透明密钥
PublicKey publicKey = keyFactory.generatePublic(keySpec);
对称密钥工厂:SecretKeyFactory
这个类与KeyFactory的作用相识,只是它是用来转换对称密钥的透明与不透明表示
2.4.4 基于口令的密钥生成和密钥共享
基于口令的密钥生成和密钥共享代表两种获取输入信息并输出单个秘密的方式。 它们有不同的用途。 在第一种情况下,我们获取用户知道的东西,并使用它来导出我们要使用的秘密信息。 在第二种情况下,我们从要使用的秘密信息开始,然后通过将其一部分提供给不同的用户来隐藏它,当需要恢复原始秘密,其中一些人必须合作(同时需要他们手中的部分信息)。
基于口令的密钥生成
在“采取易于记忆的方式并产生有效的对称密钥”的情况下,存在一系列利用消息摘要和随机盐生成密钥的算法。 有多种技术可以做到这一点,但是大多数技术都是围绕消息摘要构建的。 这些功能统称为基于口令的密钥派生功能,或简称PBKDF
。 使用由PBKDF
生成的密钥完成的加密简称为基于口令的加密或PBE
(PBE下面讲加密的时候讲)。
PKDF的输入
除口令外,PBKDF
还需要盐和迭代次数,并且需要定义一个伪随机函数(PRF
)。 PRF
充当混合器,将密码,口令和迭代次数结合在一起,并用于从PBKDF
生成字节流,然后通常将其用于创建对称加密算法的密钥。 PRF
通常是消息摘要或HMAC
。
口令
在PBKDF
的输入中,只有口令是秘密的。 重要的是要意识到口令是最终密钥熵(随机性)的来源。 与往常一样,密钥的熵是确定任何加密安全性的主要因素之一。
换句话说,您可以使用PBKDF
从单个字符口令轻松生成256位流。 但是,如果口令的范围在“ a”到“ z”(含)之间,则仅26256位流是可能的-实际上,其安全强度小于5 位。
所以禁止使用非常短的口令
另一个考虑因素是,口令始终会转换为字节数组,以输入PRF
并与其他组件混合。 值得一提的是,这种转换是如何发生的。 尽管有些算法会将一个字符视为16位,而另一些算法会在处理之前将字符转换为UTF-8
,但有些算法会将给定的所有输入转换7位ASCII。 这一点很重要,因为如果您尝试利用非英语字符集甚至扩展字符集,则需要确保PBKDF
使用的转换器是正确的。
Salt盐
盐是一种公共参数。 盐总是一个随机的字节串。
盐可以是任何长度,但最好使其至少与PRF
输出单个块时所产生的输出大小一样大。 通常,此大小与用于支持PRF
本身的相关HMAC
或消息摘要的输出长度相同。 这意味着,可能用于攻击PBKDF
的任何预先计算的哈希表“彩虹表”的大小必须与PRF
输出的位长相同。 这将使创建足够多的表以进行有意义的字典攻击变得极为困难,即使不是不可能。
迭代次数
迭代次数也是一个公共参数。 迭代次数的唯一目的是增加将口令转换为密钥所需的计算时间,这种想法是,如果使用100而不是1的迭代计数,则执行该计算的成本将增加100倍 。
在2000年,迭代次数为1024似乎足够大。 如今,在可接受的计算延迟下,适当增大迭代次数。
PBKDF2算法
创建PBKDF
的最常见技术是PKCS#5
中描述的第二种方案,称为PBKDF2
。PBKDF2
使用HMAC
作为PRF
。
示例
//JCE实现PBKDF2
public static byte[] jcePKCS5Scheme2(char[] password, byte[] salt,int iterationCount)
throws GeneralSecurityException{
SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2WITHHMACSHA256","BC");
return fact.generateSecret(new PBEKeySpec(password, salt, iterationCount, 256)).getEncoded();
}
//BC库底层API实现
public static byte[] bcPKCS5Scheme2(char[] password, byte[] salt,int iterationCount)
{
//实例化一个PBE参数生成器,指定的哈希算法将被用于HMAC
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(new SHA256Digest());
//初始化,指定编码器、盐和迭代次数
generator.init(
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password),
salt,
iterationCount);
//指定密钥长度
return ((KeyParameter)generator.generateDerivedParameters(256)).getKey();
}
密钥共享
**示例:**好像是没有给专用的API,可以自己实现秘密共享的功能。
/**
* 用于承载被分割的秘密信息的各个部分
*/
public class SplitSecret{
private final BigInteger[] shares;
/**
*
*
* @param shares 被分割的秘密信息的各个部分
*/
public SplitSecret(BigInteger[] shares) {
this.shares = shares.clone();
}
/**
* 获取各部分秘密信息的一个拷贝
*
* @return an array of the secret's shares.
*/
public BigInteger[] getShares(){
return shares.clone();
}
}
/**
* shamir秘密共享
*/
public class ShamirSecretSplitter{
private final int numberOfPeers;//秘密信息参与方,决定将秘密信息分成多少部分
private final int k; //门限值
private final BigInteger order;
private final SecureRandom random;
private final BigInteger[] alphas;
private final BigInteger[][] alphasPow;
/**
* 在指定字段上创建一个 ShamirSecretSplitter 实例,以在指定数量的对等方之间共享秘密
*
* @param numberOfPeers 要共享秘密的对等方个数
* @param threshold 用于恢复秘密的所需对等点个数
* @param field 代表我们正在操作的组的主要字段。
* @param random 随机源
*/
public ShamirSecretSplitter(
int numberOfPeers, int threshold, BigInteger field,SecureRandom random){
this.numberOfPeers = numberOfPeers;
this.k = threshold;
this.order = field;
this.random = random;
// Pre-calculate powers for each peer.
alphas = new BigInteger[numberOfPeers];
alphasPow = new BigInteger[numberOfPeers][k];
if (k > 1){
for (int i = 0; i < numberOfPeers; i++){
alphas[i] = alphasPow[i][1] = BigInteger.valueOf(i + 1);
for (int degree = 2; degree < k; degree++){
alphasPow[i][degree] = alphasPow[i][degree - 1].multiply(alphas[i]);
}
}
}else{
for (int i = 0; i < numberOfPeers; i++){
alphas[i] = BigInteger.valueOf(i + 1);
}
}
}
/**
* Given the secret, generate random coefficients (except for a<sub>0</sub>
* which is the secret) and compute the function for each privacy peer
* (who is assigned a dedicated alpha). Coefficients are picked from (0,
* fieldSize).
*给定秘密信息,生成随机系数(除了常数项a0,因为它是秘密信息)并计算每个隐私对等点(分配了专用 alpha)的函数。 系数从 (0, fieldSize) 中选取。
* @param secret 要被共享的秘密信息
* @return 秘密信息的各个部分,可以被分发共享
*/
public SplitSecret split(BigInteger secret){
BigInteger[] shares = new BigInteger[numberOfPeers];
BigInteger[] coefficients = new BigInteger[k];
// D0: for each share
for (int peerIndex = 0; peerIndex < numberOfPeers; peerIndex++){
shares[peerIndex] = secret;
}
//把秘密信息设置为多项式系数的常数项
coefficients[0] = secret;
// D1 to DT: for each share
for (int degree = 1; degree < k; degree++){
BigInteger coefficient = generateCoeff(order, random);
//逐个生成多项式的系数
coefficients[degree] = coefficient;
for (int peerIndex = 0; peerIndex < numberOfPeers; peerIndex++){
shares[peerIndex] = shares[peerIndex].add(
coefficient.multiply(alphasPow[peerIndex][degree]).mod(order)
).mod(order);
}
}
return new SplitSecret(shares);
}
/*
Shamir 的原始论文实际上将集合 [0, fieldSize) 作为可以选择系数的范围,这对于最高阶项而言并非如此,因为它会降低多项式的阶数。 我们通过使用 set (0, fieldSize) 来防止这种情况,因此消除了 0 的机会。
*/
private static BigInteger generateCoeff(BigInteger n, SecureRandom random){
int nBitLength = n.bitLength();
BigInteger k = new BigInteger(nBitLength, random);
//生成不为0的且小于order的大整数
while (k.equals(BigInteger.ZERO) || k.compareTo(n) >= 0){
k = new BigInteger(nBitLength, random);
}
return k;
}
}
2.5 算法参数表示
每种算法都有一些基本的算法参数,如对称加密中的初始向量,非对称加密算法中的群参数等。Java也为这些参数提供了专用的类来表示:AlgorithmParameters
和AlgorithmParameterSpec
类,分别为参数的不透明和透明表示。他们的区别与密钥的透明和不透明表示的区别相似。
参数的透明表示
AlgorithmParameterSpec接口
这个接口不包含任何方法,只用于分组和提供类型安全
他有很多实现类,对应不同算法的参数表示:
• DHParameterSpec
• DHGenParameterSpec
• DSAParameterSpec
• ECGenParameterSpec
• ECParameterSpec
• GCMParameterSpec
• IvParameterSpec
• MGF1ParameterSpec
• OAEPParameterSpec
• PBEParameterSpec
• PSSParameterSpec
• RC2ParameterSpec
• RC5ParameterSpec
• RSAKeyGenParameterSpec
参数的不透明表示
AlgorithmParameters类
它是提供算法参数的不透明表示的引擎类,可以使用它的getInstance
方法传入算法名称类获取特定的实例,然后用参数的透明表示来初始化它。它可以用来转换参数的透明与不透明表示。
//GCMParameters是bc库的ASN.1格式的GCM参数表示 GCMParameters extends ASN1Object
GCMParameters asn1Params = GCMParameters.getInstance(pGCM.getEncoded("ASN.1"));
AlgorithmParameters pGCM = AlgorithmParameters.getInstance("GCM", "BC");
//使用AlgorithmParameters的init方法,使用经过编码的getEncoded()GCM的参数来初始化这个参数对象
pGCM.init(asn1Params.getEncoded());
算法参数生成器
AlgorithmParameterGenerator类
他是一个引擎类,可以为指定的算法类型生成全新的算法参数。以GCM为例(认证加密)
AlgorithmParameterGenerator pGen = AlgorithmParameterGenerator.getInstance("GCM","BC");
AlgorithmParameters pGCM = pGen.generateParameters();
GCMParameterSpec gcmSpec = pGCM.getParameterSpec(GCMParameterSpec.class);
gcmSpec.getIV();
gcmSpec.getTLen();
2.6 消息摘要
MessageDigest
消息摘要算法(哈希)的引擎类,调用其getInstance
方法以获得特定算法的实例
//获取实例
MessageDigest md5 = MessageDigest.getInstance("MD5");
//压入待计算的数据
md5.update("data".getBytes());
//执行摘要算法,获得摘要
byte[] digest = md5.digest();
2.7 加密
Cipher类
Cipher
类是用于提供加解密服务的引擎类,包括对称和非对称加密,可以使用getInstance
方法和适当的参数来获取具体的加解密实例对象。
对称加密算法
在获取Cipher实例时指定一个对称加密算法,然后使用一个对称密钥进行初始化。这里需要传入的参数为:算法名称/工作模式/填充模式
//生成一个对称密钥
KeyGenerator aes = KeyGenerator.getInstance("AES");
aes.init(128);
SecretKey key = aes.generateKey();
//获取一个加密实例
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key);
//加密数据
byte[] enBytes = cipher.doFinal("data".getBytes());
//解密
//设置为解密模式
cipher.init(Cipher.DECRYPT_MODE,key);
byte[] bytes = cipher.doFinal(enBytes);
特别注意类似CBC加密这种需要初始向量的加密模式
//生成一个对称密钥
KeyGenerator aes = KeyGenerator.getInstance("AES");
aes.init(128);
SecretKey key = aes.generateKey();
//获取一个加密实例
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
//初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key);
//拿到默认随机生成的初始向量,解密时需要相同的
byte[] iv = cipher.getIV();
//加密数据
byte[] enBytes = cipher.doFinal("data".getBytes());
//解密
//设置为解密模式
cipher.init(Cipher.DECRYPT_MODE,key,new IvParameterSpec(iv));
byte[] bytes = cipher.doFinal(enBytes);
也可以自己生成初始向量
//生成一个对称密钥
KeyGenerator aes = KeyGenerator.getInstance("AES");
aes.init(128);
SecretKey key = aes.generateKey();
//获取一个加密实例
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
//生成初始向量
SecureRandom secureRandom = new SecureRandom();
byte[] iv = new byte[cipher.getBlockSize()];
secureRandom.nextBytes(iv);
//初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key,new IvParameterSpec(iv));
//加密数据
byte[] enBytes = cipher.doFinal("data".getBytes());
//解密
//设置为解密模式
cipher.init(Cipher.DECRYPT_MODE,key,new IvParameterSpec(iv));
byte[] bytes = cipher.doFinal(enBytes);
也可以这么做
//加密放从Cipher对象中获取算法参数对象,并编码发送给解密方,这个算法参数里包含IV初始向量
byte[] encodedParams = cipher.getParameters().getEncoded();
//解密方恢复算法参数对象,并使用它来初始化自己的Cipher对象
AlgorithmParameters aesParams =
AlgorithmParameters.getInstance("AES");
aesParams.init(encodedParams);
Cipher aliceCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, priKey, aesParams);
注意:解密时用的参数要与加密时用的参数相同,否则解密失败。如初始向量要相同。这就涉及如何安全传输加密参数。可以使用SealedObject类来加密参数对象。
非对称加密算法
获取Cipher类实例时,传入非对称加密算法的名称来获得对应非对称加密算法的加密实例。
//生成密钥对
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
rsa.initialize(1024);
KeyPair keyPair = rsa.generateKeyPair();
//获取Cipher实例,如要指定公钥加密的填充模式,方式与对称加密相同,如RSA/None/PKCS1Padding
Cipher cipher = Cipher.getInstance("RSA");
//初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE,keyPair.getPublic());
//加密
byte[] bytes = cipher.doFinal("data".getBytes());
//解密
//设置为解密模式
cipher.init(Cipher.DECRYPT_MODE,keyPair.getPrivate());
byte[] bytes1 = cipher.doFinal(bytes);
加密流
CipherInputStream
和CipherOutputStream
。
这两个类继承自过滤流FilterInputStream
和FilterOutputStream
,它会对读入的或者输出的流进行特殊的处理。这里就是对这些数据流进行加解密处理。
//输入流
FileInputStream fis = new FileInputStream("/tmp/a.txt");
//cipher是一个加解密对象,加密或解密模式都行
CipherInputStream cis = new CipherInputStream(fis, cipher);
//缓冲数组,接收cis中经过处理后的数据
byte[] b = new byte[1024];
//读进数据
int i = cis.read(b);
Password-Based Encryption (PBE) 基于口令的加解密算法
虽然PBE算法也是对称加密算法,但是他的加解密步骤与一般的对称加解密有些许不同,这里单独拿出来举例。PBE加密算法不是一种全新的加解密算法,而是整合了已有的对称加密算法和哈希函数,组合除了一个能够使用短口令(Password)来生成加密密钥的加密算法,它的设计初衷就是能够接收一个容易记忆的短口令作为密钥源。它的加解密参数有两个:随机盐和迭代轮数。随机盐是用来解决短口令安全性不足的问题。
PBE算法在加密过程中并不是直接使用口令来加密,而是加密的密钥由口令生成,这个功能由PBE算法中的KDF函数完成。KDF函数的实现过程为:将用户输入的口令首先通过“盐”(salt)的扰乱产生准密钥,再将准密钥经过散列函数多次迭代后生成最终加密密钥,密钥生成后,PBE算法再选用对称加密算法对数据进行加密。
//短口令
char[] password = "password".toCharArray();
//使用随机数生成器生成随机盐
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] salt = random.generateSeed(8);
//迭代轮数
int count = 100;
//根据口令生成密钥,其实就是把这个口令包装一下,没有生成新的密钥
//将口令包装为密钥透明表示
PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
//转换为不透明表示
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWITHMD5andDES");
SecretKey key = factory.generateSecret(pbeKeySpec);
//将PBE所需的参数包装为参数对象
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, count);
//实例化Cipher对象
Cipher cipher = Cipher.getInstance("PBEWITHMD5andDES");
//使用参数初始化为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key,pbeParameterSpec);
//执行加密
byte[] enBytes = cipher.doFinal("data".getBytes());
//执行解密
//将cipher初始化为解密模式,注意使用相同的参数
cipher.init(Cipher.DECRYPT_MODE,key,pbeParameterSpec);
byte[] deBytes = cipher.doFinal(enBytes);
当然,如果不指定参数,提供者会提供默认生成的参数,这是要注意的是要提取默认生成的参数供解密程序使用:
//短口令
char[] password = "password".toCharArray();
//根据口令生成密钥,其实就是把这个口令包装一下,没有生成新的密钥
//将口令包装为密钥透明表示
PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
//转换为密钥不透明表示
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWITHMD5andDES");
SecretKey key = factory.generateSecret(pbeKeySpec);
//实例化Cipher对象
Cipher cipher = Cipher.getInstance("PBEWITHMD5andDES");
//使用参数初始化为加密模式
cipher.init(Cipher.ENCRYPT_MODE,key);
//提取参数,供解密程序使用
AlgorithmParameters parameters = cipher.getParameters();
//执行加密
byte[] enBytes = cipher.doFinal("data".getBytes());
//执行解密
//将cipher初始化为解密模式,注意使用相同的参数
cipher.init(Cipher.DECRYPT_MODE,key,parameters);
byte[] deBytes = cipher.doFinal(enBytes);
密钥包装
Cipher
类还提供了用于包装(wrap)/解包装(unwrap)一个密钥的方法,可以用一个密钥加密另一个密钥:
// 创建一个要被包装的密钥
KeyGenerator generator = keyGenerator.getInstance("AES", "BC");
generator.init(128);
Key keyToBeWrapped = generator.generateKey();
// 创建一个在包装时进行加密的密钥
KeyGenerator keyGen = KeyGenerator.getInstance("AES", "BC");
KeyGen.init(256);
Key wrapKey = keyGen.generateKey();
//创建一个Cipher用于包装密钥,并使用包装密钥初始化
Cipher cipher = Cipher.getInstance("AESKWP", "BC");
cipher.init(Cipher.WRAP_MODE, wrapkey);
//进行包装,获得密文字节数组
byte[] wrappedKey = cipher.wrap(KeyToBeWrapped);
// 解包装
cipher.init(Cipher.UNWRAP_MODE, wrapkey);
Key key = cipher.unwrap(wrappedkey, "AES", Cipher.SECRET_KEY);
Cihper
类定义了三个常量给解包装方法使用,分别代表不同的密钥类型:Cipher.PUBLIC_KEY
, Cipher.PRIVATE_KEY
, 和 Cipher.SECRET_KEY
认证加密算法
认证加密算法是加密算法+消息认证码,拥有保密性和可认证,以GCM为例
//加密
static byte[] gcmEncryptWithAAD(SecretKey key,
byte[] iv,
byte[] pText,
byte[] aData) throws Exception{
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
//设置认证码的长度和初始向量
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
//设置关联数据Associated Data,也可以不设置
cipher.updateAAD(aData);
return cipher.doFinal(pText);
}
//解密
static byte[] gcmDecryptWithAAD(SecretKey key,byte[] iv,byte[] cText,byte[] aData)throws Exception{
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
//可以使用bc库提供的AEADParameterSpec类直接带上关联数据
AEADParameterSpec spec = new AEADParameterSpec(iv, 128, aData);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(cText);
}
加密一个对象 SealedObject 类
JCE
除了提供用于调用密钥包装的API
外,还提供了一个简单的类来允许携带和恢复加密的对象。 做到这一点的类是javax.crypto.SealedObject
。
SecretKey aesKey = createConstantKey();
// 创建一个可序列化的对象
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", "BC");
kpGen.initialize(2048);
KeyPair kp = kpGen.generateKeyPair();
//用于加密的
Cipher wrapCipher = Cipher.getInstance("AES/CCM/NoPadding", "BC");
//aesKey为AES的密钥
wrapCipher.init(Cipher.ENCRYPT_MODE, aesKey);
// 创建一个 SealedObject
SealedObject sealed = new SealedObject(kp.getPrivate(), wrapCipher);
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
ObjectOutputStream oOut = new ObjectOutputStream(bOut);
oOut.writeObject(sealed);
oOut.close();
SealedObject transmitted = (SealedObject)new ObjectInputStream(
new ByteArrayInputStream(bOut.toByteArray())).readObject();
//取出被加密的对象,使用加密时的密钥
PrivateKey unwrappedKey =
(PrivateKey)transmitted.getObject(aesKey, "BC");
可以看出,我们只要把待加密的对象(可序列化的)和Cipher的一个对象(设置为加密模式)传给SealedObject即可。SealedObject自动为我们做对象的加解密。
2.8 数字签名
Signature类
它是提供数字签名算法的引擎类,使用getInstance
方法获取指定算法的实例对象。
//生成密钥对
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
rsa.initialize(1024);
KeyPair keyPair = rsa.generateKeyPair();
//获取签名对象
Signature sha224withRSA = Signature.getInstance("SHA224withRSA");
//初始化,设置为签名模式
sha224withRSA.initSign(keyPair.getPrivate());
//执行签名
sha224withRSA.update("data".getBytes());
byte[] sign = sha224withRSA.sign();
//验证
//初始化为验证模式
sha224withRSA.initVerify(keyPair.getPublic());
//压入信息数据
sha224withRSA.update("data".getBytes());
//验证签名
boolean verify = sha224withRSA.verify(sign);
2.9 消息认证码
Mac类
这个类是提供消息认证码服务的引擎类,使用getInstance
方法获取指定算法的实例对象。
// 生成密钥
KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256");
SecretKey key = kg.generateKey();
// 获取实例
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte[] result = mac.doFinal("data".getBytes());
2.10密钥协商
密钥协商是一种协议,根据该协议,双方或多方可以建立相同的加密密钥,而无需交换任何机密信息。它是一个引擎类,可以使用getInstamce
方法获取指定算法的实例对象。
每个参与方都用自己的私钥初始化自己的密钥协议对象,然后为将参与通信的其他方的公钥输入。对应的方法在下面的例子中用到时讲解:
//两个参与方的密钥协商(例子来源于官方文档)
/**------------------Alice---------------------**/
//Alice使用2048位密钥大小创建自己的DH密钥对
System.out.println("ALICE: 生成DH密钥对 ...");
KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");
aliceKpairGen.initialize(2048);
KeyPair aliceKpair = aliceKpairGen.generateKeyPair();
// Alice 创建并初始化KeyAgreement对象
System.out.println("ALICE: 创建KeyAgreement");
KeyAgreement aliceKeyAgree = KeyAgreement.getInstance("DH");
System.out.println("ALICE: 初始化KeyAgreement");
//初始化时需要输入自己的私钥,密钥协商协议的伪随机数生成器也是需要的,如果不提供就使用默认生成器
aliceKeyAgree.init(aliceKpair.getPrivate());
// Alice 编码它的公钥,并发送给其他参与方
byte[] alicePubKeyEnc = aliceKpair.getPublic().getEncoded();
/**------------------Bob---------------------**/
//Bob收到了Alice的编码的公钥,并将其回恢复
KeyFactory bobKeyFac = KeyFactory.getInstance("DH");
X509EncodedKeySpec x509KeySpec = new
X509EncodedKeySpec(alicePubKeyEnc);
PublicKey alicePubKey = bobKeyFac.generatePublic(x509KeySpec);
//Bob获取与Alice的公钥关联的DH参数。生成自己的密钥对时,他必须使用相同的参数。
DHParameterSpec dhParamFromAlicePubKey = ((DHPublicKey) alicePubKey).getParams();
// Bob生成他自己的密钥对
System.out.println("BOB:使用算法参数生成DH密钥对");
KeyPairGenerator bobKpairGen = KeyPairGenerator.getInstance("DH");
//使用和Alic相同的算法参数初始化
bobKpairGen.initialize(dhParamFromAlicePubKey);
KeyPair bobKpair = bobKpairGen.generateKeyPair();
// Bob 生成并初始化他自己的KeyAgreement对象
System.out.println("BOB: 生成KeyAgreement");
KeyAgreement bobKeyAgree = KeyAgreement.getInstance("DH");
System.out.println("BOB: 初始化KeyAgreement");
//使用Bob自己的密钥初始化
bobKeyAgree.init(bobKpair.getPrivate());
// Bob 也编码他的公钥并发送给Alic
byte[] bobPubKeyEnc = bobKpair.getPublic().getEncoded();
//爱丽丝在她的DH协议版本的第一阶段(也是唯一阶段,需要的阶段数与参与方的个数有关)使用鲍勃的公钥。
//在这样做之前,她必须从Bob的编码密钥材料中实例化DH公钥。
KeyFactory aliceKeyFac = KeyFactory.getInstance("DH");
x509KeySpec = new X509EncodedKeySpec(bobPubKeyEnc);
PublicKey bobPubKey = aliceKeyFac.generatePublic(x509KeySpec);
//密钥协商协议包含多个阶段,这个方法用进入并执行各个阶段,它需要传入这个阶段需要的密钥,和表明这个阶段是否是最终阶段
//如果是第一阶段,则传入的密钥是其他方的公钥,或者是其他方产生的中间密钥对象(doPhase有一个Key类型的返回值)
//因为只有两个参与方,只需要导入一次公钥,所以只需要执行一次doPhase方法,不需要接收其返回值。
System.out.println("ALICE: 执行阶段1,即导入Bob的公钥");
//Alice导入Bob的公钥
aliceKeyAgree.doPhase(bobPubKey, true);
//Bob导入Alice的公钥
System.out.println("BOB: Execute PHASE1 ...");
bobKeyAgree.doPhase(alicePubKey, true);
//在此阶段,Alice和Bob都已完成DH密钥协商协议。两者都生成(相同)共享秘密。
//Alice获取她的共享密钥
byte[] aliceSharedSecret = aliceKeyAgree.generateSecret();
byte[] bobSharedSecret = bobKeyAgree.generateSecret();
System.out.println("Alice shared secret: " + Base64.toBase64String(aliceSharedSecret));
System.out.println("Bob shared secret: " + Base64.toBase64String(bobSharedSecret));
if (!java.util.Arrays.equals(aliceSharedSecret, bobSharedSecret)){//判断共享密钥是否相同
throw new Exception("Shared secrets differ");
}
System.out.println("双方生成的共享密钥相同,密钥协商成功");
//使用共享的密钥生成特定于算法的加密密钥
System.out.println("使用生成的共享密钥进行加解密...");
//生成128比特(16字节)分组大小的AES的密钥,这个方法的意思就是取共享密钥的前128比特作为AES的密钥。
System.out.println("生成AES密钥...");
SecretKeySpec bobAesKey = new SecretKeySpec(bobSharedSecret, 0, 16, "AES");
SecretKeySpec aliceAesKey = new SecretKeySpec(aliceSharedSecret, 0, 16, "AES");
System.out.println("Alice AES secret: " + Base64.toBase64String(aliceAesKey.getEncoded()));
System.out.println("Bob AES secret: " + Base64.toBase64String(bobAesKey.getEncoded()));
//各自执行加解密。。。。。
三个参与方进行密钥协商的例子见官方文档
2.11 密钥管理
称为“密钥存储库(keystore)”的数据库是可用于管理密钥和公钥证书的存储库。
密钥存储库文件位置
默认情况下,用户密钥存储库存储在用户主目录中名为.keystore的文件中,由用户决定。默认值取决于操作系统的主系统属性user.home
:
- Linux and macOS:
/home/username/
- Windows:
C:\Users\username\
当然,可以根据需要定位密钥存储库文件。在某些环境中,存在多个密钥存储库是有意义的。例如,一个密钥存储库可能持有用户的私钥,而另一个可能持有用于建立信任关系的证书。
除了用户的密钥存储库之外,JDK还维护一个系统范围的密钥存储库,用于存储来自各种证书颁发机构(CA)的可信证书。这些CA证书可用于帮助进行信任决策。例如,在SSL/TLS/DTLS中,当SunJSSE提供者收到来自远程对等方的证书时,默认的trustmanager将查阅以下文件之一来确定连接是否可信:
- Linux and macOS:
<java-home>/lib/security/cacerts
- Windows:
<java-home>\lib\security\cacerts
注意:这个文件路径是Java9模块化之后的路径,之前的路径应该是<java-home>\jre\lib\security\cacerts
应用程序可以不使用系统范围的cacerts密钥存储库,而是设置和使用它们自己的密钥存储库,甚至可以使用上面描述的用户密钥存储库。
密钥存储库的实现
KeyStore
类提供了定义良好的接口来访问和修改密钥库中的信息。可能存在多个不同的具体实现,其中每个实现针对特定类型的密钥存储库。
目前,有两种命令行工具可以使用密钥存储库:keytool和jarsigner。由于密钥存储库是公开可用的,JDK用户可以编写使用它的其他安全应用程序。
应用程序可以使用KeyStore
类中的getInstance 工厂方法从不同的提供者程序中选择不同类型的密钥库实现。密钥存储库的类型定义了密钥存储库信息的存储和数据格式,以及用于保护密钥存储库中的私有密钥和密钥存储库本身的完整性的算法。不同类型的密钥存储库实现是不兼容的。
Java8往后默认的密钥存储库实现是“pkcs12”,Java8是jks(这些国际标准会在下一章讲到),可以通过KeyStore类的getDefaultType()方法来获取默认的密钥库类型。也可以修改配置文件的以下条目来修改默认的密钥库类型:
keystore.type=pkcs12
注:配置文件位置在:java8:<JAVA_HOME>\jre\lib\security\java.security;java9以后:<JAVA_HOME>\conf\security\java.security
JDK实现的密钥库类型有:
- "jceks":“jceks”是“jks”的另一种专用密钥存储库格式,它使用基于口令的(PBE)三重DES加密。“jceks”实现可以解析并将“jks”密钥库文件转换为“jceks”格式。您可以通过更改密钥存储库中私有密钥条目的密码并指定“-storetype jceks”作为密钥存储库类型,将类型“jks”的密钥存储库升级为类型“jceks”的密钥存储库。
- “jks”:它将密钥存储库实现为文件,使用专用的密钥存储库类型(format)。库中的每个密钥都有独立的密码保护,还使用(可能不同的)密码保护整个密钥存储库的完整性。
- **“dks”**是一个域密钥库。它是作为单个逻辑密钥库表示的密钥库集合。
- “pkcs12”:这是一个基于RSA PKCS12个人信息交换语法标准的跨平台密钥存储库。这个标准主要用于存储或传输用户的私钥、证书和其他秘密。任意属性可以与PKCS12密钥存储库中的单个条目相关联。
KeyStore类
KeyStore类提供了定义良好的接口来访问和修改密钥库中的信息。他是一个引擎类,即可以使用getInstance
方法获得实例。
这个类表示密钥和证书的内存集合。KeyStore
管理两种类型的条目:
- Key Entry: 这种类型的密钥库条目保存非常敏感的加密密钥信息,必须对其进行保护,防止未经授权的访问。通常,存储在这类条目中的密钥是一个对称密钥,或者是一个私钥,它伴随着认证相应公钥的证书链。
- Trusted Certificate Entry: 这种类型的条目包含一个属于另一方的公钥证书。之所以称其为受信任证书,是因为密钥存储库所有者相信证书中的公钥确实属于证书的主体(所有者)标识的身份。
密钥存储库中的每个条目都由一个“别名”字符串标识。对于私钥及其相关的证书链,这些字符串可以区分实体进行自身身份验证的不同方式。例如,实体可以使用不同的证书颁发机构或使用不同的公钥算法对自己进行身份验证。
**这里没有指定密钥存储库是否是持久的,以及如果是持久的,密钥存储库所使用的机制。**这个约定允许使用各种技术来保护敏感密钥(例如,私有密钥或秘密密钥)。智能卡或其他集成的密码引擎(SafeKeyper)是一种选择,还可以使用更简单的机制,如文件(以各种格式)。
注:还有一个相似的类CertStore
,它用来存储非受信的公钥证书。
KeyStore的方法(建议看源码上的注释,很详细)
-
获取实例:
getInstance
工厂方法用来获取一个KeyStore实例,参数传入密钥库的类型。也可以指定服务提供者:KeyStore instance = KeyStore.getInstance(KeyStore.getDefaultType());
-
加载一个密钥库进内存:
final void load(InputStream stream, char[] password)
方法将特定密钥存储库加载到内存中。可以提供密码以解锁密钥库(例如,密钥库位于硬件令牌设备上),或检查密钥库数据的完整性。如果未提供用于完整性检查的密码,则不会执行完整性检查。如果这个密钥库已经被加载过,则会重新加载。如果需要创建一个空密钥存储库,可以将null作为InputStream参数传递给load方法。
final void load(LoadStoreParameter param)
方法通过给定的参数加载密钥库,LoadStoreParameter接口参数类型用于指定如何加载密钥库,它的子类实现了提供加载密钥库所需参数的方法,可以为null。如通过将DomainLoadStoreParameter传递给这个方法来加载DKS密钥存储库 -
获取密钥库种的所有别名:
final Enumeration<String> aliases()
。所有密钥库项都是通过惟一的别名访问的。aliases方法返回密钥存储库中别名的枚举。 -
判断指定别名对应的密钥库条目类型(一共两种类型)
final boolean isKeyEntry(String alias)
与final boolean isCertificateEntry(String alias)
方法用于判断密钥库中某一条目的类型。 -
添加/设置/删除密钥存储库条目:
-
final void setCertificateEntry(String alias, Certificate cert)
方法将证书分配给指定的别名。如果这个别名不存在,则创建具有该别名的受信任证书条目。如果别名存在并标识了一个受信任的证书条目,则cert将替换旧的证书条目。 -
设置Key条目,如果别名不存在则新加,否则替换
//将给定密钥分配给给定别名,并用给定密码保护它。 final void setKeyEntry(String alias,//别名 Key key,//要关联到别名上的Key,可以是对称密钥(SecretKey)和私钥(PrivateKey) char[] password,//保护这个密钥的口令,访问时需要提供 Certificate[] chain)//如果Key参数是一个PrivateKey,那么这个chain对应Key的证书链,否则为Null final void setKeyEntry(String alias,//别名 byte[] key,//要关联到别名上的Key,可以是对称密钥(SecretKey)和私钥(PrivateKey) Certificate[] chain)//如果Key参数是一个PrivateKey,那么这个chain对应Key的证书链,否则为Null
-
final void deleteEntry(String alias)
方法删除对应别名的条目。 -
PKCS #12密钥库支持包含任意属性的条目。使用
PKCS12Attribute
类创建属性。在创建新的密钥库条目时,使用接受属性的构造函数方法。最后,使用以下方法将条目添加到密钥存储库中:final void setEntry(String alias, Entry entry, ProtectionParameter protParam)
KeyStore类内置了三个Entry接口的实现,分别为
PrivateKeyEntry
、SecretKeyEntry
、TrustedCertificateEntry
,其作用是自解释的。
-
-
获取密钥库条目
-
final Key getKey(String alias, char[] password)
方法返回绑定到给定别名上的密钥条目,如果有访问口令的话,需要提供访问口令 -
final Certificate getCertificate(String alias)
与final Certificate[] getCertificateChain(String alias)
方法返回给定别名绑定的证书或证书链 -
PKCS #12密钥库支持包含任意属性的条目。使用以下方法检索可能包含属性的条目:
final Entry getEntry(String alias, ProtectionParameter protParam)
-
-
持久化密钥库到外部存储
//将内存中的密钥库存储到指定流中,password用于计算附加到密钥存储库数据的密钥存储库数据的完整性校验和。 final void store(OutputStream stream, char[] password) //DKS密钥库是通过将DomainLoadStoreParameter传递给这个存储方法来存储 final void store(KeyStore.LoadStoreParameter param)
其他方法自行查看KeyStore API
示例
新建一个KeyStore
//获取一个JCEKS类型密钥库实例,注意只有JCEKS支持存储SecretKey。
KeyStore keyStore = KeyStore.getInstance("JCEKS");
//load的第一个参数(外部keystore的输入流)和第二个参数设置为null,表明创建一个新的keystore
keyStore.load(null,null);
//生成一个对称密钥,用于存储
KeyGenerator aes = KeyGenerator.getInstance("AES");
SecretKey key = aes.generateKey();
//使用setKeyEntry方法存储
keyStore.setKeyEntry("key1",key,"password".toCharArray(),null);
//使用setEntry方法存储
//使用SecretKeyEntry包装密钥
KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(key);
//包装访问这个key的密钥
KeyStore.ProtectionParameter parameter = new KeyStore.PasswordProtection("password".toCharArray());
//存入
keyStore.setEntry("key2",entry,parameter);
//输出到磁盘文件
FileOutputStream fos = new FileOutputStream("E:\\key.keystore");
keyStore.store(fos,"password".toCharArray());
读取刚才建的KeySyore
//创建对象
KeyStore keyStore = KeyStore.getInstance("JCEKS");
//读取KeyStore,提供密钥库的口令
keyStore.load(new FileInputStream("E:\\key.keystore"),"password".toCharArray());
//包装访问key2需要的口令
KeyStore.ProtectionParameter parameter = new KeyStore.PasswordProtection("password".toCharArray());
//获取密钥库中所有条目的别名
Enumeration<String> aliases = keyStore.aliases();
//遍历
while (aliases.hasMoreElements()){
String s = aliases.nextElement();
//这里我们知道这里边就两个密钥,所以可以这样判断
if (keyStore.isKeyEntry(s)) {
Key key = keyStore.getKey(s, "password".toCharArray());
}else {
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry)keyStore.getEntry(s, parameter);
}
}
2.12 证书工厂
证书工厂类CertificateFactory
定义了证书工厂的功能,他是一个引擎类,用于从它们的编码生成证书和证书撤销列表(CRL)对象,就是将已存在的证书文件读入内存并生成相关的Java对象供Java程序使用。这个类合并到下一章讲PKI时叙述。
2.13 密钥传输(数字信封)
密钥传输是一个术语,用于描述在本地生成对称密钥后将对称密钥安全地传递给另一方的过程。 不考虑后量子算法,RSA
和 ElGamal
是仅有的两种直接提供这种能力的算法,因为它们的构造允许在一端加密单个纯文本块,然后在另一端恢复原始纯文本块。使用2.7节 密钥包装小节类似的方法包装密钥并传输。
RSA-OAEP
OAEP
是RSA算法的一种填充模式,他可以实现RSA
的非确定性加密模式。OAEP
的详细说明自行搜索。
/**
* 使用RSA的OAEP模式包装一个需要传输的对称密钥
*
* @param rsaPublic 用于加密的RSA公钥.
* @param secretKey 被加密的对称密钥.
* @return .
*/
public static byte[] keyWrapOAEP(PublicKey rsaPublic, SecretKey secretKey)
throws GeneralSecurityException{
//注意使用的工作模式
Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPwithSHA256andMGF1Padding", "BC");
cipher.init(Cipher.WRAP_MODE, rsaPublic);
//使用wrap方法包装
return cipher.wrap(secretKey);
}
/**
* 使用私钥解密
*
* @param rsaPrivate RSA的私钥
* @param wrappedKey 被包装的对称密钥的密文
* @param keyAlgorithm对称密钥的类型
* @return 拆包后的密钥.
*/
public static SecretKey keyUnwrapOAEP(PrivateKey rsaPrivate, byte[] wrappedKey, String
keyAlgorithm)throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPwithSHA256andMGF1Padding", "BC");
// 使用私有解密
cipher.init(Cipher.UNWRAP_MODE, rsaPrivate);
return (SecretKey)cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
}
ElGamal-OAEP
/**
* 显示基于 ElGamal OAEP 的密钥包装和解包的简单示例。
*/
public class OAEPExampleWithElGamal{
/**
* 使用基于提供程序的参数生成 2048 位 DH 密钥对。
*
* @return DH KeyPair
*/
public static KeyPair generateDHKeyPair() throws GeneralSecurityException{
KeyPairGenerator keyPair = KeyPairGenerator.getInstance("DH", "BC");
keyPair.initialize(2048);
return keyPair.generateKeyPair();
}
/**
* 使用 OAEP 算法生成一个封装的密钥,返回生成的加密。
*
* @param dhPublic 加密用的公钥.
* @param secretKey 被加密的对称密钥.
* @return .
*/
public static byte[] keyWrapOAEP(PublicKey dhPublic, SecretKey secretKey)
throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("ElGamal/NONE/OAEPwithSHA256andMGF1Padding", "BC");
cipher.init(Cipher.WRAP_MODE, dhPublic);
return cipher.wrap(secretKey);
}
/**
* 解密
*
* @param dhPrivate 解密用私钥.
* @param wrappedKey 被加密的对称密钥.
* @param keyAlgorithm 对称密钥的类型.
* @return the unwrapped SecretKey.
*/
public static SecretKey keyUnwrapOAEP(PrivateKey dhPrivate, byte[] wrappedKey,
String keyAlgorithm)throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("ElGamal/NONE/OAEPwithSHA256andMGF1Padding", "BC");
cipher.init(Cipher.UNWRAP_MODE, dhPrivate);
return (SecretKey)cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
}
}
RSA-KEM
NIST SP 800-56B
和 RFC 5990
中描述了 RSA-KEM
密钥传输算法,其中详细介绍了它在加密消息语法 (CMS
) 中的使用。 这是在最高安全级别上最接近理想使用 RSA
算法的方法,RSA-KEM
具有比 RSA-OAEP
更严格的安全性。
/**
* 使用 RSA-KTS-KEM-KWS 算法生成一个封装的密钥,返回生成的加密密钥。
*
* @param rsaPublic the public key to base the wrapping on.
* @param ktsSpec 密钥传输参数.
* @param secretKey the secret key to be encrypted/wrapped.
* @return .
*/
public static byte[] keyWrapKEMS(PublicKey rsaPublic, KTSParameterSpec ktsSpec,
SecretKey secretKey)throws GeneralSecurityException{
//注意加密算法类型
Cipher cipher = Cipher.getInstance("RSA-KTS-KEM-KWS", "BCFIPS");
cipher.init(Cipher.WRAP_MODE, rsaPublic, ktsSpec);
return cipher.wrap(secretKey);
}
/**
* 解密
*
* @param rsaPrivate the private key to use for the unwrap.
* @param ktsSpec 传输参数.
* @param wrappedKey the encrypted secret key.
* @param keyAlgorithm the algorithm that the encrypted key is for.
* @return the unwrapped SecretKey.
*/
public static SecretKey keyUnwrapKEMS(PrivateKey rsaPrivate, KTSParameterSpec ktsSpec,
byte[] wrappedKey, String keyAlgorithm)throws GeneralSecurityException{
Cipher cipher = Cipher.getInstance("RSA-KTS-KEM-KWS", "BCFIPS");
cipher.init(Cipher.UNWRAP_MODE, rsaPrivate, ktsSpec);
return (SecretKey)cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
}
三、PKI编程手册—公钥证书的生成与管理
这一章安排如下:3.1小节介绍公钥基础设施(PKI)和数字/公钥证书的基本概念;第二节将Java提供的与数字证书相关的API及其使用方法;第三节介绍密钥管理工具kettool
的基本用法。
需要了解的一些协议与规范,知道它是干啥的就行,辅助下文阅读
OCSP:OCSP协议的产生是用于在公钥基础设施(PKI
)体系中替代证书吊销列表(CRL
)来查询数字证书的状态,OCSP
克服了CRL
的主要缺陷:必须经常在客户端下载以确保列表的更新。
3.1 数字证书与公钥基础设施
3.1.1 公钥基础设施
为什么需要公钥基础设施
公钥基础设施(PKI)是一种基于公开密钥算法的安全基础标准,它提供了一个框架,在这个框架内建立了可以创建鉴定和验证过程需要的身份和相关信任关系,建立了可以管理的公开密钥加密系统。
为什么需要PKI呢?PKI是用来解决如何确定一个公钥确实是属于我们认为的另一方的。公开密钥算法的密钥对能够实现对特定密钥对的鉴别和验证, 但是无法建立将特定密钥对跟具体的个人身份联系起来的可信任关系。
如Alice与Bob通信时收到一份签名过的数据,数据中还带有验证时需要的公钥。那么如果验证通过,Alice是否就能相信这份数据是Bob用他的私钥签名发送过来的呢?这是不行的,因为如果黑客劫持了Alice与Bob通信的信道,黑客就可以给Alice发送用他自己的私钥签名过的数据给Alice,并带上他自己的公钥,这同样可以验证成功。问题的根源在于我们不能确定一个公钥是否确实是属于我们认为的另一方。如果Alice与Bob通信时,能够确信这个数据包中的公钥就是Bob的,那么就不会出现上述问题。
公钥基础设施正是为了建立这种信任关系而产生的, 它的主要目的就是建立可信任的数字身份, 将特定密钥对和特定的人或实体联系起来, 建立这种联系的主要形式就是颁发可信任的数字证书(或者叫电子证书)。
公钥基础设施的组件
公钥基础设施并不是一个单一的设施, 它由一系列的组件组成以完成其特定的功能。在一个PKI的作用域中, 并非下面介绍的所有设施都是必须的, 有些设施如RA可能并不需要。
-
验证机构(CA)。CA可以说是 PKI系统中的核心机构, 负责确认身份和创建数字证书, 建立 一个身份和一对密钥之间的联系。 作为一个程序员或技术人员, 通常可能将CA跟证书签发服务器(或证书签发应用程序)等同起来, 事实上并非如此。 CA是一个软硬件和服务的集合,包括了人、 操作流程、 操作规程和验证策略、 支持的软硬件及环境。 一个成功的CA必须制定一些规则, 使申请者和证书用户确信该CA所确认的身份适用于自己的目的并且是可信任的。 一个CA是否能够获得成功, 可能更重要的是在于其管理因素而不是技术因素。
-
注册机构(RA)。RA负责证书申请人的资料登记和初始的身份鉴别, 还可能需要接受证书用户提出的证书撤销等其他服务。 事实上, RA是一个可选的组件,在很多时候, 它所负责的功能并不需要独立出来, 而是可以成为证书服务器的一部分。一般来说, RA最主要的职责就是接受申请人的申请请求, 确认申请人的身份, 然后将确认了身份的申请请求递交给CA。
-
证书服务器。证书服务器是负责根据注册过程中提供的信息生成证书的计算机或服务程序。 证书服务器将用户的公钥和其他一些信息形成证书结构并用CA的私钥进行签名,从而生成正式的数字证书。 事实上, 证书服务器还可能要完成其他一些功能, 比如证书的存放、 发布及吊销等操作。 技术人员在谈到CA时, 通常就是指证书服务器。
-
证书库。任何证书在使用之前, 必须将证书及其相应的公钥公布出去。 证书库就是存储可以公开发布的证书的设施。 通常以目录的形式组成 PKI的证书库, 如
x.500
目录或者LDAP
目录。 目前最为常见的是LDAP目录协议, 它由一组对目录中定位信息的方法和协议描述组成,是一个通信协议。 -
证书验证。当证书用户收到一个证书时, 需要对这个证书进行验证。 证书验证的项目通常包括:
- 验证证书的签名者以确认是否信任该证书;
- 检测证书的有效期, 确验证书是否有效;
- 确验证书没有被签发它的CA撤销;
- 检测证书预期用途跟CA在该证书中指定的策略是否相符合。
证书的验证过程通常是对证书链的验证, 这通常要执行多个上述项的循环验证以得到最终验证结果。 证书的验证可以由客户端的验证程序执行, 也可以提供专门的验证服 务, 客户端可以通过使用这种服务来完成验证。
-
等。。。
3.1.2 什么是数字证书
数字证书正是为了建立实体跟密钥对之间的联系而存在,证书验证中心CA充当了确认特定实体跟密钥对之间关系的确认人,并且通过用自己的私钥对这些确认的信息和公钥一起签名来保证其可信性和不可改变性。 这里的前提是,CA是所有用户都信任的。
也就是说,证书是一个可信的机构(CA)颁发给某一个体的“数字身份证”,其他个体能够通过验证CA在这个证书上设置的防伪标识(CA用它自己的私钥对证书的签名)来确定这个证书是否是真的属于特定个体的,确信之后再从证书中提取这个个体的公钥和其他一些信息。
3.1.3 数字证书的生命周期
证书申请
用户自己在本地生成一对特定于算法的密钥对(如RSA),然后他就可以把他的公钥和一些其他身份相关的信息打包,再用私钥签名发送给CA,CA验证后(包括用户身份的验证等)会生成相应的数字证书。但对于具体的操作或者技术来说,用户需要填写哪些信息及怎么格式化这些信息是一个需要考虑的问题,PKCS#10
标准规定了这些相关的格式。其基本思想是用户根据要求填写这些信息,然后这些信息和用户产生的公钥一起使用相应的私钥签名发送给CA验证。
证书颁发
拿到证书请求后,CA首先验证证书请求上的签名是否正确,也就是说,确保公钥对应的私钥就在申请者手中,并且申请信息是正确的没有被更改的。然后就是仔细检查证书请求中的用户信息,这一工作一般由CA的工作人员完成,工作人员应该依据管理规范对这些用户信息进行审查和核实,必要的时候甚至需要亲自查访用户等。如果通过了所有这些严格的审查措施,那么 CA就可以给你签发证书了。
证书验证
在使用一个数字证书时,首先需要验证这个证书是否是有效的。包括验证:CA 的数字签名、证书的有效期、证书是否被吊销及其他一些可能的限制选项。
证书吊销
在某些情况下,一个人的证书可能需要提前被吊销(证书到期之前)。证书吊销的操作从技术上来说包含连个方面:一是从CA的证书数据库中删除被吊销的证书;二是对外公布被吊销的证书信息,如序列号等, 具体来说就是生成和公布证书吊销列表CRL。
证书过期
证书的使用有 一定的期限,这是为了确保证书安全性和有效性的需要。证书过期后,CA需要更新证书库中已经过期的证书的状态,比如对过期证书归档和将其从有效证书库中删除等操作。
3.1.4 数字证书的封装类型
X.509证书
x.509
证书包含的内容主要是用户信息、证书序列号、签发者、有效期、公钥、其他信息及CA的数字签名。我们 需要确认的是, x.509
只包含了一个公钥,而没有这个公钥对应的私钥的任何信息。公钥是可以公开的,所以x.509
证书一般也是随意公开的, 任何人都可以获取你的x.509
证书,然后使用它来跟你通信。比如在SSL协议中, 在服务器要跟你建立安全信道的时候,就会给你发送它自己的 x.509
证书,以便证明自己的身份,这是必要的, 也是安全的, 它不用担心你会利用它的 x.509
冒充它欺骗其他用户,因为你没有x.509
证书里面公钥相应的私钥,达不到这样的目的。x.509
证书适用于一般的证书应用模式。
PKCS# 12证书
一般来说,证书相应的私钥应该保存在硬件中,如智能卡或者U盾中以确保其安全性,计算机存储总是让人不放心。 但是很多时候由于条件限制或者安全性要求不足以让单位的决策者掏出这么一笔钱买这些设备,那么就必须把证书和其相应的私钥保存在内存中,这就对证书和私钥的存放提出了复杂的要求。你可能需要在不同的计算机上使用相同的证书,当然,也需要相应的私钥,如果把它们保存在不同的文件中,有可能因为弄错了多个证书和私钥之间的对应关系而导致没有办法使用,所以最好是把它们保存在一起。
PKCS# 12
格式证书就是为了适应上述的这些需求产生的,它将证书和其相应的私钥封装在一起。 当然,证书和私钥需要的安全性是不一样的,证书可以公开,所以不需要加密保存;而私钥的安全性非常重要,PKCS#12
采用了PKCS#8
的私钥封装格式对私钥进行了基于口令的加密,虽然这种安全性很值得怀疑,但是毕竟有了一些保护。
PKCS#12
证书中既有私钥也有公钥,我们可以把他转换成一个X.509
证书和一个私钥。
PKCS#7 证书
证书的验证可能不是一个简单的事情。如Alice的证书是由CA-1签发的,它的证书拿给Bob验证,但是Bob并没有CA-1这个机构的公钥的可信拷贝(就是Bob不能验证CA-1的签名),那么Bob就可以去查看有没有其他CA机构(CA-2)构给CA-1颁发过数字证书,如果有,而且这个Bob信任CA-2的证书,那么他就可以用CA-2的公钥验证CA-1的证书。这就形成了一个证书链:从待验证的个体的证书往上,直到查找到Bob信任的CA或者根CA签发的证书。(关于证书链,3.1.5节会补充讲解)
如上图,你和你的验证用户可能隶属于不同的CA,但是这些不同的CA可能属于相同的根CA,那么验证就需要更多的信息,不仅仅需要你的CA证书,还需要签发你的CA证书的上级CA证书,直到上溯到根CA证书,也就是说,验证的时候,验证方会需要一个完整的证书链。 所谓证书链,就是一个用户证书和一系列与其证书相关的CA证书的有序集合。 所以,考虑到用户这些需求,为了使证书用户能够正确地使用自己的证书,CA在给用户颁发证书的时候,不仅仅要给用户发放用户自己的证书,还可能需要把证书链中的所有CA证书都给用户。
上面所说的验证过程对于一般情形来说还太理想了, 事实上,一 个CA 或多或少会有一些由于各种原因被吊销的证书, 那么为了排除这些已经被吊销的证书就需要通知用户哪些证书被吊销了,这一般通过证书吊销列表(CRL)来实现的。CRL 是公开的,可以自由下载。
这样一来, 用户需要给验证方提供的东西就非常多了: 自己的证书、 证书链上所有的CA证书和CRL。这么多东西一个个发送给对方是很繁琐的,因为他必须鉴别你发送过来的是什么东西, 以及考虑怎么保存这些信息。 为了避免上述所有这些尴尬的情况,PKCS#7
标准的证书格式出现了。
PKCS#7
标准很简单:可以包含多个证书和CRL。 这样就可以解决上述的问题了,在验证的时候,把自己的证书、 相关证书链上的CA 证书和CRL封装成一个PKCS#7
格式证书发送给验证方就可以了。
3.1.5 重要知识点补充
前面几小节大概介绍了数字证书的基本概念,下面就几个核心重要的知识点再做一些补充说明。
证书链
证书链对于验证证书是否可信来说是重中之重,下面根据官方文档中描述的形式再对证书链做补充叙述。
如果用户不具有签署主体的公钥证书的CA的公钥的受信任副本,则需要另一个用于签名CA的公钥证书。这句话的意思如下图:
最左边是Alice的CA2给他颁发的数字证书,当Bob拿到Alice的证书,他需要CA2的公钥来验证Alice的证书中CA2的签名。但是如果Bob没有CA2的公钥的可信副本(CA2的可信公钥副本即被Bob信任这个公钥就是CA2的),那么他就不能验证CA2生成的签名。现在的问题就变成了怎么验证CA2的数字证书(即用于签署Alice的数字证书的CA2的私钥对应的公钥证书,上图中间)是可信的。验证CA2的证书是否可信就需要CA1的公钥,如果刚好Bob拥有CA1的可信公钥(即确信自己拥有CA1的公钥),那么他就可以验证CA2的由CA1生成的数字证书。这就形成了一条链。事实上这条链可以更长,直到延申到Bob最信任的CA机构(称为信任锚)的证书或者根CA的证书。
必须先验证证书链,然后才能依赖它建立对主体公钥的信任。验证可以包括对证书链中包含的证书的各种检查,例如验证签名和检查每个证书是否已被撤销。PKIX标准定义了一种验证由X.509证书组成的证书链的算法。
通常,用户可能没有从最受信任的CA到主体的证书链。提供构建或发现证书链的服务是公钥系统的一个重要特性。RFC 2587给出了一个LDAP
(轻量级目录访问协议)模式定义,该定义使用LDAP
目录服务协议促进了X.509
证书链的发现。
X.509数字证书
公钥证书是一个实体的数字签名声明,声明另一个实体的公钥和其他信息具有特定的值。x.509
标准定义了什么信息可以进入证书,并描述了如何将其写入证书(数据格式)。所有的X.509
证书除了签名外,还有以下数据:
-
版本号。这标识了
X.509
标准的哪个版本适用于此证书,这将影响其中可以指定哪些信息。到目前为止,已经定义了三个版本。 -
序列号。创建证书的实体负责为它分配一个序列号,以区别于它颁发的其他证书。此信息可用于多种方式,例如当证书被撤销时,将其序列号放置在证书撤销清单(CRL)中。
-
签名算法标识符。这标识CA用于签署证书的算法。
-
颁发者名称。签署证书的实体的
X.500
(构成全球分布式的名录服务系统的协议)名称。这通常是一个CA。使用此证书意味着信任签署此证书的实体。(请注意,在某些情况下,例如根证书或顶级CA证书,颁发者签署自己的证书。) -
有效期。每个证书的有效期都是有限的。这个时期由开始日期和时间以及结束日期和时间来描述,可以短到几秒钟,也可以长到几乎一个世纪。选择的有效期取决于许多因素,例如用于签署证书的私钥的强度或人们愿意为证书支付的金额。如果关联的私钥没有被泄露,实体可以在此期间依赖公共值。
-
主体名。证书标识其公钥的实体的名称。这个名称使用了
X.500
标准,因此它在Internet上是惟一的。这是实体的专有名称(DN),例如:CN=Java Duke, OU=Java Software Division, O=Sun Microsystems Inc,C=US
(这些指的是主体的通用名称、组织单位、组织和国家。)
-
主体的公钥信息。这是被命名实体的公钥,以及指定此密钥属于哪个公钥加密系统的算法标识符和任何相关的密钥参数。
X.509
的三个版本
X.509
版本1自1988年开始使用,被广泛部署,是最通用的。X.509
版本2引入了主体和发布方唯一标识符的概念,以处理随着时间的推移重用主体和/或发布方名称的可能性。大多数证书配置文件强烈建议不要重用名称,证书不应该使用唯一标识符。版本2证书没有被广泛使用。X.509
版本3是最新的(1996年),它支持扩展的概念,因此任何人都可以定义一个扩展并将其包含在证书中。目前使用的一些常见扩展有:KeyUsage
(限制密钥的使用目的,如“仅签名”)和AlternativeNames
(允许其他身份也与此公钥关联,如DNS名称、电子邮件地址、IP地址)。可以将扩展标记为critical,以指示应该检查和强制/使用该扩展。
证书中的所有数据都使用两个相关的标准进行编码,称为ASN.1/DER
。Abstract Syntax Notation 1(ASN.1
)用于描述数据(即编码)。Distinguished Encoding Rules(DER)描述了存储和传输数据的单一方式。
证书管理工具keytool
可以用于生成、显示、导入、导出X.509
证书。
3.2 数字证书核心API
本节的内容比较繁杂,所以这些内容基本是官方文档的整理和翻译,以防止有内容的遗漏
Java证书链API定义了用于创建、构建和验证证书链的接口和抽象类。可以使用基于提供程序的接口插入实现。该API基于前面章节中描述的密码服务提供者体系结构,并包括根据PKIX
标准构建和验证X.509
证书链的特定算法类。PKIX
标准是由IETF PKIX工作组开发的。
Java证书链API的核心类由以独立于算法和实现的方式支持证书链功能的接口和类组成。该API构建并扩展了现有的java.security.cert
处理证书软件包。核心类可分为4类:基本类、验证、构建和存储。
Java证书链API
还包括一组RFC 5280中定义的PKIX
证书链验证算法相关的类。
证书链 API
中的大多数类和接口都不是线程安全的。
3.2.1 基本的证书链相关类
基本证书链类提供了编码和表示证书链的基本功能。Java证书链中的关键基本类API
是CertPath
,它封装了由所有类型的证书链共享的通用方法。应用程序可使用CertificateFactory
类的实例来创建(不是新建,是根据**已有的证书链(可能是个文件)**的编码格式转换为CertPath
对象)CertPath
对象。
3.2.1.1 CertPath类
CertPath
类是证书链的抽象类。它定义了所有证书链对象共享的功能。可以通过对CertPath
类进行子类化来实现各种证书链类型,尽管它们可能具有不同的内容和排序模式。
所有的CertPath
对象都是可序列化的、不可变的和线程安全的,并且具有以下特征:
-
一个类型:这对应于证书链中的证书类型,例如:
X.509
。CertPath
的类型通过以下方法获得:public String getType()
-
证书列表:
getCertificates
方法返回证书链中的证书列表public abstract List<? extends Certificate> getCertificates()
这个方法返回一个包含0个或多个
java.security.cert.Certificate
类型对象的列表。为了保护CertPath
对象的内容,返回的列表和其中包含的证书是不可变的。返回的证书的顺序取决于类型。按照约定,类型为X.509
的CertPath
对象中的证书顺序是从目标证书开始,到信任锚(一个可信的CA)颁发的证书结束。也就是说,一个证书的颁发者是下一个证书对应的CA。表示受信任者的证书不应该包含在证书路径中(授信CA的证书在验证者自己手中)。PKIX
的CertPathValidator
类将检测任何偏离这些约定的核心类和接口,从而导致认证路径无效,并抛出CertPathValidatorException
异常。 -
一种或者多种编码:每个
CertPath
对象支持一种或多种编码。这些是证书链的外部编码形式,当需要在Java虚拟机外部使用证书链的标准表示形式时(比如通过网络将证书路径传输到其他方时)使用。每个证书链都可以用默认的格式进行编码,编码过的字节数组使用以下方法返回:public abstract byte[] getEncoded()
另外,以下重载方法返回指定的编码格式:
public abstract byte[] getEncoded(String encoding)
以下方法返回受支持的编码格式名称
public abstract Iterator<String> getEncodings()
CertPath
对象是使用CertificateFactory
从已编码的字节数组或证书列表生成的。或者,可以使用CertPathBuilder
来尝试查找从最信任CA到特定主体的CertPath
。一旦创建了CertPath
对象,就可以通过将其传递给CertPathValidator
的验证方法对其进行验证。这些概念将在后面的部分中更详细地解释。
3.2.1.2 CertificateFactory类
CertificateFactory
类是一个引擎类,它定义了证书工厂的功能。它使用编码过的证书数据来生成证书、CRL
和CertPath
对象。即将一个已存在的证书材料解析为证书对象。
注意:JCA没有提供直接生成公钥证书的API,都是将已存在的证书数据导入进程序并用证书对象表示。要在java代码中根据给定的公钥信息生成一个公钥证书,请使用BC库中提供的API
CertificateFactory对象的创建
CertificateFactory
类是一个引擎类,可以使用它的某一个getInstance
方法来获取它的对象,同时指定证书的格式。比如说一个X.509
的证书工厂必须返回java.security.cert.X509Certificate
类型的证书对象和java.security.cert.X509CRL
类型的撤销列表对象。
//创建产生X.509证书对象的证书工厂
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
生成证书对象
使用generateCertificate
方法可以从一个包含已编码的证书的流中生成证书对象
final Certificate generateCertificate(InputStream inStream)
要返回从给定输入流中读取的证书的集合(可能是空的),可以使用generateCertificates
方法:
final Collection<? extends Certificate> generateCertificates(InputStream inStream)
生成CRL对象
使用generateCRL
方法可以从一个包含已编码的证书撤销列表的流中生成CRL对象:
final CRL generateCRL(InputStream inStream)
要生成从给定输入流中读取的CRL的集合(可能是空的),可以使用generateCRLs
方法:
final Collection generateCRLs(InputStream inStream)
生成CertPath对象
要从输入流中读取的数据生成CertPath
对象,请使用以下generateCertPath
方法之一(指定或不指定数据使用的编码格式):
final CertPath generateCertPath(InputStream inStream)
final CertPath generateCertPath(InputStream inStream,String encoding)
要从一个Certificate
证书对象列表中生成CertPath
对象,可以使用下面的方法:
final CertPath generateCertPath(List<? extends Certificate> certificates)
CertificateFactory
总是返回由与工厂类型相同的证书组成的CertPath
对象。例如,类型为X.509
的CertificateFactory
返回由证书组成的CertPath
对象,这些证书是java.security.cert.X509Certificate
的一个实例。
注意这个方法跟
generateCertificates
方法的区别,这个方法由一系列确定能组成证书链的证书来生成证书链;而generateCertificates
方法用于将一系列并不确定有什么关系的证书的编码流读取成一个证书列表
查看此工厂支持的编码格式
final Iterator<String> getCertPathEncodings()
例子
从文件流中加载证书对象
首先使用3.3节的keytool
工具生成并导出公钥证书(3.3节的示例),命令行如下:
#生成证书并存储到KeyStore中
keytool -genkeypair -keyalg RSA -alias liu -keystore E:\\liu.keystore
#在从KeyStore导出证书
keytool -exportcert -alias liu -keystore E:\\liu.keystore -file E:\\liu.cer -rfc
然后就可以使用CertificateFactory
的对象加载证书到内存中:
//获取对象
CertificateFactory instance = CertificateFactory.getInstance("X.509");
//生成证书对象
X509Certificate certificate = (X509Certificate) instance.generateCertificate(
new FileInputStream("E:\\liu.cer"));
从KeyStore
中获取CertPath
对象
// 实例化一个JKS类型的KeyStore
KeyStore ks = KeyStore.getInstance("JKS");
// 加载KeyStore文件中内容
ks.load(new FileInputStream("./keystore"),"password".toCharArray());
//找到别名为sean的证书链
Certificate[] certArray = ks.getCertificateChain("sean");
// 转为List集合类型
List certList = Arrays.asList(certArray);
// 获取一个X.509类型的证书工厂
CertificateFactory cf = CertificateFactory.getInstance("X.509");
//由证书列表构造CertPath对象
CertPath cp = cf.generateCertPath(certList);
3.2.1.3 CertPathParameters接口
CertPathParameters
接口是特定证书链构建器或验证算法所使用的一组参数的透明表示,标记接口。
它的主要目的是分组(并为)所有证书链参数规范提供类型安全。CertPathParameters
接口继承了Cloneable
接口,并定义了一个不会抛出异常的clone()
方法。这个接口的所有具体实现都应该实现并覆盖Object.clone()
方法(如果需要的话)。这允许应用程序克隆任何CertPathParameters
对象。
实现CertPathParameters接口的对象可作为参数传递给CertPathValidator
和CertPathBuilder
类的方法。通常,CertPathParameters
接口的具体实现将包含一组特定于特定证书链构建器或验证算法的输入参数。例如,PKIXParameters
类是CertPathParameters
接口的一个实现,该接口保存一组用于PKIX
认证路径验证算法的输入参数。其中一个参数是调用者为验证过程提供的最受信任的CA集合。这个参数将在讨论PKIXParameters
类的部分中进行更详细的讨论。
3.2.2 证书链的验证类
Java证书链API
包括用于验证证书链的类和接口。应用程序使用CertPathValidator
类的实例来验证CertPath
对象,如果成功了那么就会把验证结果封装到实现了CertPathValidatorResult
接口的类对象里。
3.2.2.1 CertPathValidator类
CertPathValidator
类是一个用于验证证书链的引擎类。
创建CertPathValidator类的实例
使用如下getInstance
方法之一来创建一个实例
public static CertPathValidator getInstance(String algorithm)
public static CertPathValidator getInstance(String algorithm, String provider)
public static CertPathValidator getInstance(String algorithm, Provider provider)
其中algorithm
是证书链验证算法的名称,如PKIX
。
验证一个证书链
一旦创建了CertPathValidator
对象,就可以通过调用validate
方法来验证,向它传递要验证的证书链和一组特定于算法的参数:
public final CertPathValidatorResult validate(CertPath certPath, CertPathParameters params)
throws CertPathValidatorException,InvalidAlgorithmParameterException
如果验证算法成功,结果将在实现CertPathValidatorResult
接口的对象中返回。否则,将抛出CertPathValidatorException
异常。CertPathValidatorException
包含返回CertPath
的方法,如果相关的话,还返回导致算法失败的证书索引以及导致失败的根异常或根源。
注意,传递给验证方法的CertPath
和CertPathParameters
必须是验证算法支持的类型。否则,将抛出InvalidAlgorithmParameterException
异常。例如,实现PKIX
算法的CertPathValidator
实例验证类型为X.509
的CertPath
对象和作为PKIXParameters
实例的CertPathParameters
。
3.2.2.2 CertPathValidatorResult接口
接口描述
CertPathValidatorResult
接口是证书路径验证算法的成功结果或输出的透明表示,标记接口。
此接口的主要目的是对所有验证结果进行分组并提供类型安全。类似于CertPathParameters
接口,CertPathValidatorResult
继承了Cloneable
并定义了一个不会抛出异常的clone()
方法。这允许应用程序克隆任何CertPathValidatorResult
对象。
实现CertPathValidatorResult
接口的对象在验证成功时由validate
方法返回。如果不成功,将抛出一个CertPathValidatorException
异常。
通常,CertPathValidatorResult
接口的具体实现将包含一组特定于特定证书链验证算法的输出参数。例如,PKIXCertPathValidatorResult
类是CertPathValidatorResult
接口的一个实现,该接口包含用于获取PKIX
证书链验证算法的输出参数的方法,这些方法包括获取验证成功后获取主体公钥的方法。
示例(官方文档里的)
// 创建一个实现PKIX验证算法的实例
CertPathValidator cpv = null;
try {
cpv = CertPathValidator.getInstance("PKIX");
} catch (NoSuchAlgorithmException nsae) {
System.err.println(nsae);
System.exit(1);
}
// 使用验证参数“params”验证证书链,假设参数params已存在,这个参数将在后面讲PKIX时详细说明
//cp参数是我们要验证的一个证书链CertPath对象
try {
CertPathValidatorResult cpvResult = cpv.validate(cp, params);
} catch (InvalidAlgorithmParameterException iape) {//参数异常
System.err.println("validation failed: " + iape);
System.exit(1);
} catch (CertPathValidatorException cpve) {//验证失败
System.err.println("validation failed: " + cpve);
System.err.println("index of certificate that caused exception:"+ cpve.getIndex());
System.exit(1);
}
3.2.3 证书链的构建类
前两个小节讲的都是当证书链已存在时对证书链的操作,本小节将介绍如何去构建一个新的证书链。
Java证书链API
包括用于构建(或发现)证书链的类。应用程序使用CertPathBuilder
类的实例来构建CertPath
对象。如果成功,构建的结果将在实现CertPathBuilderResult
接口的对象中返回。
3.2.3.1 CertPathBuilder类
CertPathBuilder
类是用于从一堆证书集合中筛选并构建一个有效的(验证通过的)证书链的引擎类。
不应该将CertificateFactory
与CertPathBuilder
混淆。CertPathBuilder
用于发现或查找尚不知道的证书链。相反,当证书链已经被发现并且调用者需要从其内容(该内容以不同的形式存在,如已编码的字节数组或证书数组)实例化一个CertPath
对象时,可以使用CertificateFactory
。
获得CertPathBuilder的实例
与其他所有引擎类一样,使用getInstance
方法并传入算法名称参数来获得CertPathBuilder
的实例
public static CertPathBuilder getInstance(String algorithm)
public static CertPathBuilder getInstance(String algorithm, String provider)//指定服务提供者
public static CertPathBuilder getInstance(String algorithm, Provider provider)//指定服务提供者
algorithm
参数代表要使用的证书链构造算法的算法名称,如PKIX
。
构建一个证书链
拿到CertPathBuilder
的实例后,就可以调用build
方法并传入算法相关的参数来构造一个证书链:
//这个参数里包含了一个或多个证书集合(如一个CertStore对象,远程的或本地的),证书链的构建都是在这里查找证书,后面讲PKIX会讲到
public final CertPathBuilderResult build(CertPathParameters params)
throws CertPathBuilderException, InvalidAlgorithmParameterException
如果构建算法成功,结果将在实现CertPathBuilderResult
接口的对象中返回。否则,抛出包含失败信息的 CertPathBuilderException
异常。
请注意,传递给构建方法的CertPathParameters
必须是构建算法支持的类型。否则抛出InvalidAlgorithmParameterException
异常。
3.2.3.2 CertPathBuilderResult类
CertPathBuilderResult
接口是证书链构建器算法的结果或输出的透明表示。此接口包含一个方法(除了clone()
方法外只有一个),用于返回已成功构建的认证路径:
public CertPath getCertPath()
CertPathBuilderResult
接口的目的是对所有构建结果进行分组(并为其提供类型安全)。与CertPathValidatorResult
接口相似,
CertPathBuilderResult
扩展了Cloneable
并定义了一个不会抛出异常的clone()
方法。这允许应用程序克隆任何CertPathBuilderResult
对象。
CertPathBuilderResult
是一个CertPathBuilder
对象的build
方法的返回值。
示例(官方文档上的例子)
//创建一个实现PKIX构建算法的构建器
CertPathBuilder cpb = null;
try {
cpb = CertPathBuilder.getInstance("PKIX");
} catch (NoSuchAlgorithmException nsae) {
System.err.println(nsae);
System.exit(1);
}
// 使用验证参数“params”验证证书链,假设参数params已存在,这个参数将在后面讲PKIX时详细说明
try {
CertPathBuilderResult cpbResult = cpb.build(params);//构建,得到结果
CertPath cp = cpbResult.getCertPath();//从结果里拿出证书链对象
System.out.println("build passed, path contents: " + cp);
} catch (InvalidAlgorithmParameterException iape) {//传入的算法参数异常
System.err.println("build failed: " + iape);
System.exit(1);
} catch (CertPathBuilderException cpbe) {//构建失败
System.err.println("build failed: " + cpbe);
System.exit(1);
}
3.2.4 证书/CRL的存储类
本节涉及证书链和撤销列表的存储相关类CertStore
和与之相关的一些类。与之前讲的KeyStore
不同,KeyStore
可用于存储经过验证的、可信的证书或者证书链,而CertStore
则存储一些公共的甚至是可能存在大量不相关且通常不受信任的证书集合。
3.2.4.1 CertStore类
CertStore
类是一个引擎类,用于提供证书和证书撤销列表(CRL
)存储库的功能。
CertStore
类可以被CertPathBuilder
和CertPathValidator
的实现来查找证书和CRL
,或者用作通用的证书和CRL
检索机制。
CertStore
对象的所有公共方法都是线程安全的。也就是说,多个线程可以在单个(或多个)CertStore
对象上并发地调用这些方法,而不会产生不良影响。
创建CertStore对象
使用getInstance
方法:
public static CertStore getInstance(String type, CertStoreParameters params)
public static CertStore getInstance(String type, CertStoreParameters params, String provider)
public static CertStore getInstance(String type, CertStoreParameters params, Provider provider)
type
参数代表要使用的证书库类型的名称,如LADP
(远程库)。初始化参数(params
)特定于存储库类型。例如,基于服务器的存储库的初始化参数可能包括服务器的主机名和端口号。如果传入的参数与当前的证书库的类型不匹配,则会抛出InvalidAlgorithmParameterException
异常。
可以使用CertStore
类的以下方法来获取用于初始化这个CertStore
对象的证书库参数:
public final CertStoreParameters getCertStoreParameters()
在CertStore中检索证书
当你获得了一个CertStore
证书库对象后,你就可以使用它的getCertificates
方法来检索里面的证书,并为其传入一个CertSelector
对象(后面讲)来对证书进行筛选。CertSelector
对象中制定了一系列筛选标准,用于判断哪些证书应该被返回:
final Collection<? extends Certificate> getCertificates(CertSelector selector) throws CertStoreException
这个方法返回匹配成功的证书集合(可能是空的),当出现一些异常情况导致检索失败时(如与远程库的链接断开),则通常会抛出CertStoreException
异常。
对于某些CertStore
的实现,在整个存储库中通过匹配规则搜索证书或CRL
可能是不可行的。在这些情况下,CertStore
实现可能使用选择器中指定的信息来定位证书和CRL
。例如,LDAP CertStore
(使用LDAP
协议的远程证书库)可能不会搜索目录中的所有条目。相反,它可能只搜索可能包含它正在寻找的证书的条目。如果提供的CertSelector
没有为LDAP CertStore
提供足够的信息来确定它应该查找哪些条目,那么LDAP CertStore
可能会抛出CertStoreException
。
在CertStore中检索CRL
与检索证书的方法相识,检索CRL使用方法getCRLs
,并为其传入匹配规则CRLSelector
对象:
final Collection<? extends CRL> getCRLs(CRLSelector selector) throws CertStoreException
3.2.4.2 CertStoreParameters接口
CertStoreParameters
接口是特定CertStore
使用的一组参数的透明表示。
此接口的主要目的是对所有证书存储参数规范进行分组并提供类型安全。接口扩展了Cloneable
接口,并定义不会抛出异常的克隆方法。
实现CertStoreParameters
接口的对象可作为参数传递给CertStore
类的getInstance
方法。有两个实现
CertStoreParameters
接口的类:LDAPCertStoreParameters
类和CollectionCertStoreParameters
类。
LDAPCertStoreParameters类
LDAPCertStoreParameters类是CertStoreParameters
接口的实现,它包含一组最小的初始化参数(目录服务器的主机和端口号),用于从类型为LDAP
的CertStore
检索证书和CRL。(访问远端服务器的)
CollectionCertStoreParameters类
CollectionCertStoreParameters类是CertStoreParameters
接口的一个实现,它拥有一组初始化参数,用于从某类型集合的CertStore
检索证书和CRL
。(访问本地的)
3.2.4.3 CertSelector 和 CRLSelector接口
CertSelector
和CRLSelector
接口是一组标准的规范,用于从一个证书和CRL
集合中选择证书和CRL。
接口为所有选择器规范提供类型安全。每个选择器接口都扩展了Cloneable
接口。
CertSelector
和CRLSelector
接口各自定义了一个名为match
的方法。match
方法接受一个证书或CRL对象作为参数,如果该对象满足选择条件,则返回true
。否则,返回false
。
public boolean match(Certificate cert)//匹配证书
public boolean match(CRL crl)//匹配CRL
通常,实现这些接口的对象作为参数传递给CertStore
类的getCertificates
和getCRLs
方法。CertSelectors
还可以用于指定证书链中的目标证书或最终实体证书的验证约束(如PKIXParameters
的setTargetCertConstraints
方法,后面讲PKIX
的时候会讲)。
X509CertSelector类
X509CertSelector
类是CertSelector
接口的一个实现,该接口定义了一组选择X.509
证书的标准。
X509Certificate
证书对象必须匹配match
方法指定的所有选择标准。选择标准被设计用于CertPathBuilder
对象在构建X.509
证书链时发现潜在的证书。
例如,X509CertSelector
的setSubject
方法允许一个PKIX
CertPathBuilder
过滤掉与前一个x.509
证书(按照证书链的规则,后一个证书的所有者应该是前一个证书的发行者)的发行者名称不匹配的x.509
证书。通过在X509CertSelector
对象中设置此条件和其他条件,CertPathBuilder
能够丢弃不相关的证书,并更容易地找到满足CertPathParameters
对象中指定的要求的X.509
证书路径。
创建一个X509CertSelector对象
使用它的无参构造器:
public X509CertSelector()
这个构造器的方法体是空的,即没有做任何初始化相关的工作。
设置选择规则
选择条件允许调用者匹配X.509证书的不同组件。下面列举一些设置选择标准的一些方法
-
设置发行者
public void setIssuer(X500Principal issuer) public void setIssuer(String issuerDN) public void setIssuer(byte[] issuerDN)
指定的专有名称(在
X500Principal
、RFC 2253
字符串或ASN.1/DER
编码形式中)必须匹配证书中的发布者专有名称。如果为null
,任何发布者专有名称都可以。请注意,最好使用X500Principal
来表示专有名称,因为这样更有效,类型也更合适。 -
设置主体
public void setSubject(X500Principal subject) public void setSubject(String subjectDN) public void setSubject(byte[] subjectDN)
指定的专有名称(在
X500Principal
、RFC 2253
字符串或ASN.1/DER
编码形式中)必须匹配证书中的主题专有名称。如果为null
,任何主体专有名称都可以。 -
设置序列号
public void setSerialNumber(BigInteger serial)
指定的序列号必须与证书中的证书序列号匹配。如果为空,表示任何证书序列号都可以。
-
设置权威密钥标识符
public void setAuthorityKeyIdentifier(byte[] authorityKeyID)
证书必须包含与指定值匹配的权威密钥标识符扩展。如果为
null
,则不会对authorityKeyIdentifier
标准进行检查。 -
设置证书的有效期
public void setCertificateValid(Date certValid)
指定日期必须在该证书的有效期内。如果
null
,任何日期都是有效的。 -
设置密钥用途
public void setKeyUsage(boolean[] keyUsage)
证书的密钥使用扩展必须允许指定的
keyUsage
值(那些被设置为true的)。如果为null
,则不执行键使用检查。
获取选择规则
可以使用适当的getXXX
方法检索每个选择条件的当前值。
例子(源于官方文档)
下面是一个从LDAP
CertStore
使用X509CertSelector
类检索X.509
证书的示例。
//首先创建一个LDAPCertStoreParameters证书库参数对象,指定远程证书库的IP地址和端口号
LDAPCertStoreParameters lcsp = new LDAPCertStoreParameters("ldap.sun.com", 389);//这个IP我这边无法访问!
//然后创建一个LADP类型的证书库对象,并使用lcsp初始化
CertStore cs = CertStore.getInstance("LDAP", lcsp);
//下面创建一个X509CertSelector选择器对象
//创建对象
X509CertSelector xcs = new X509CertSelector();
// 设置证书期限,传入当前日期,即未过期的
xcs.setCertificateValid(new Date());
// 只选择颁发给拥有以下个人信息的证书
// 'CN=alice, O=xyz, C=us'
xcs.setSubject(new X500Principal("CN=alice, O=xyz, C=us"));
// 仅选择终端实体证书
xcs.setBasicConstraints(-2);
// 仅选择具有digitalSignature密钥使用权证书(将布尔数组中的第一个条目设置为true)
boolean[] keyUsage = {true};
xcs.setKeyUsage(keyUsage);
//仅选择subjectAltName的值为'alice@xyz.example.com'的证书,1是一个RFC822Name的整形值
xcs.addSubjectAlternativeName(1, "alice@xyz.example.com");
//然后,我们将选择器传递给之前创建的CertStore对象的getCertificates方法:
Collection<Certificate> certs = cs.getCertificates(xcs);
PKIX CertPathBuilder
可以使用类似的代码,通过丢弃那些不满足验证约束或其他标准的证书来帮助发现和排序潜在的证书。
X509CRLSelector类
X509CRLSelector
类是CRLSelector
接口的一个实现,该接口定义了一组选择X.509
CRL的标准。
创建X509CRLSelector对象
使用它的无参构造器,也是一个空构造器,不执行任何初始化操作
public X509CRLSelector()
设置选择规则
选择条件允许调用者匹配X.509
CRL的不同组件。可以自行查阅X509CRLSelector
类的API来了解,文档讲的很清楚。
例子(源于官方文档)
创建从LDAP
存储库检索CRL的X509CRLSelector
类似于X509CertSelector
示例。假设我们希望检索特定CA发出的具有最小CRL编号的所有当前(截至当前日期和时间)CRL。首先,我们创建一个X509CRLSelector
对象并调用适当的方法来设置选择条件:
//无参构造器
X509CRLSelector xcrls = new X509CRLSelector();
// 设置适合当前时间的
xcrls.setDateAndTime(new Date());
// 设置由以下发行者发行的
xcrls.addIssuerName("O=xyz, C=us");
// 设置至少有以下CRL数字的
xcrls.setMinCRLNumber(new BigInteger("2"));
//调用方法筛选获取
Collection<CRL> crls = cs.getCRLs(xcrls);
3.2.5 PKIX相关类
Java证书链API包括一组为使用PKIX
证书链验证算法的特定于算法的类。
PKIX
证书链验证算法在RFC 5280: 因特网X.509
中定义公钥基础设施证书和证书撤销列表(CRL)配置文件。
3.2.5.1 TrustAnchor类
TrustAnchor
类表示一个“最受信任的CA”,它被用作验证X.509
证书链的信任锚。TrustAnchor
包括CA的公钥、CA的名称和可以使用此密钥验证的任何约束。这些参数可以以受信任的X509Certificate
的形式指定,也可以作为单独的参数指定。TrustAnchor
是不可变和线程安全的。
创建TrustAnchor对象
要实例化TrustAnchor
对象,调用者必须将“最受信任的CA”指定为受信任的x.509
证书或 公钥/专有名称 对。调用者还可以选择指定名称约束,这个名称约束是初始化期间验证算法作用于信任锚点上的。但注意,PKIX
算法不需要对信任锚点上的名称约束的支持,因此PKIX
CertPathValidator
或CertPathBuilder
可能会选择不支持该参数,而抛出异常。
//直接传入X.509证书
public TrustAnchor(X509Certificate trustedCert, byte[] nameConstraints)
//传入名称和公钥,名称使用x.500规范
public TrustAnchor(X500Principal caPrincipal, PublicKey pubKey, byte[] nameConstraints)
//传入名称和公钥
public TrustAnchor(String caName, PublicKey pubKey, byte[] nameConstraints)
nameConstraints
参数被指定为一个包含经ASN.1
编码过的名称限制(NameConstraints
)扩展的数组。如果不能对名称约束进行解码(没有正确地格式化),则抛出IllegalArgumentException
。
获取参数值
所有构造时传入的参数都能使用相应的getXXX
方法获取
public final X509Certificate getTrustedCert()
public final X500Principal getCA()
public final String getCAName()
public final PublicKey getCAPublicKey()
public final byte[] getNameConstraints()
注意,构造的时候怎么传入的参数就要怎么获取。如传入时是一个证书对象,就不能用getCAName
方法获取CA的名称。
3.2.5.2 PKIXParameters类
public class PKIXParameters implements CertPathParameters
PKIXParameters
类实现了CertPathParameters
接口,表示由PKIX
证书链验证算法定义的一组输入参数。它还包括一些其他有用的参数。
用处举例
一个X.509
CertPath
对象和一个PKIXParameters
对象被作为参数传递给实现PKIX
算法的CertPathValidator
实例的validate
方法。CertPathValidator
使用参数初始化PKIX
证书链验证算法。
创建PKIXParameters对象
要实例化PKIXParameters
对象,调用者必须指定由PKIX
验证算法定义的“最受信任的CA”集合。最信任的CA集合可以使用以下两个构造函数之一来指定:
//允许调用者将最信任的CA集合指定为一组TrustAnchor对象。
public PKIXParameters(Set<TrustAnchor> trustAnchors) throws InvalidAlgorithmParameterException
//指定包含受信任证书条目的KeyStore实例,其中每个条目将被视为最受信任的CA。注意KeyStore与CertStore的区别
public PKIXParameters(KeyStore keystore) throws KeyStoreException, InvalidAlgorithmParameterException
设置参数值
最好自己去查阅一下PKIX的文档。
创建PKIXParameters
对象后,调用者可以设置(或替换)各种参数的值。下面描述一些设置参数的方法。有关其他方法的详细信息,请参考PKIXParameters API文档。
-
设置初始策略标识符
设置由PKIX验证算法指定的初始策略标识符。
//集合中的元素是表示为字符串的对象标识符(oid)。如果initialPolicies参数为null或未设置,则可以接受任何策略: public void setInitialPolicies(Set<String> initialPolicies)
-
设置有效期
此方法设置应确定路径有效性的时间,即确定在指定的时间点是否有效,不设置就是当前时间。
public void setDate(Date date)
-
设置策略映射抑制标志(the policy mapping inhibited flag)
此方法设置策略映射抑制标志的值。如果未指定,标志的默认值为false:
public void setPolicyMappingInhibited(boolean val)
-
设置显式策略必需标志(the explicit policy required flag),默认为false
public void setExplicitPolicyRequired(boolean val)
-
设置禁止任何策略标志(the any policy inhibited flag),默认为false
public void setAnyPolicyInhibited(boolean val)
-
设置对终端证书的限制条件
setTargetCertConstraints
方法允许调用者在目标或最终实体证书上设置约束。例如,调用者可以指定目标证书必须包含特定的主题名称。约束被指定为CertSelector
对象。如果selector
参数为null
或未设置,则在目标证书上没有定义约束:public void setTargetCertConstraints(CertSelector selector)
-
设置使用的证书库CertStore对象
setCertStores
方法允许调用者指定一个CertStore
对象列表,该列表将被CertPathValidator的
PKIX实现用于查找用于路径验证的CRL。这提供了一种可扩展的机制来指定定位CRL的位置。setCertStores
方法接受一个CertStore
对象列表作为参数。列表中的第一个CertStores可能比后面出现的优先级更高。public void setCertStores(List<CertStore> stores)
-
设置证书链检查器
setCertPathCheckers
方法允许调用者通过创建特定于实现的证书链检查器来扩展PKIX
验证算法。例如,此机制可用于处理私有证书扩展。setCertPathCheckers
方法以一列PKIXCertPathChecker
(稍后讨论)对象作为参数:public void setCertPathCheckers(List<PKIXCertPathChecker> checkers)
-
禁用撤销检查
setRevocationEnabled
方法允许调用者禁用撤销检查。默认情况下启用撤销检查,因为这是PKIX
验证算法所必需的检查。但是,PKIX
没有定义如何检查撤销。例如,实现可以使用CRLs或OCSP。如果不合适,此方法允许调用者禁用实现的默认吊销检查机制。然后可以通过调用setCertPathCheckers
方法指定不同的撤销检查机制,并传递一个实现替代机制的PKIXCertPathChecker
。public void setRevocationEnabled(boolean val)
-
启用或禁用策略限定符处理(policy qualifier processing)
setPolicyQualifiersRejected
方法允许调用者启用或禁用策略限定符处理。当创建PKIXParameters
对象时,这个标志被设置为true
。此设置反映了处理策略限定符最常见(和最简单)的策略。希望使用更复杂策略的应用程序必须将此标志设置为false。public void setPolicyQualifiersRejected(boolean qualifiersRejected)
获取参数值
可以使用相应的getXXX
方法获取设置的各种参数值。
3.2.5.3 PKIXCertPathValidatorResult类
public class PKIXCertPathValidatorResult implements CertPathValidatorResult
PKIXCertPathValidatorResult 类表示PKIX
证书链验证算法的结果。
这个类实现了CertPathValidatorResult
接口。它持有有效的验证算法生成的策略树和主体公钥(就是被验证的主体的公钥),并包含用于返回它们的方法(getPolicyTree()
和getPublicKey()
)。实现PKIX
算法的CertPathValidator
对象的验证方法返回PKIXCertPathValidatorResult
的实例。
3.2.5.4 PolicyNode接口和PolicyQualifierInfo类
PKIX
验证算法定义了几个与证书策略处理相关的输出。大多数应用程序不需要使用这些输出,但是实现PKIX
验证或构建算法的所有提供者程序都必须支持它们。
PolicyNode
接口表示有效执行PKIX
证书链验证所产生的有效策略树的节点。一个应用程序可以使用PKIXCertPathValidatorResult
的getPolicyTree
方法获取有效策略树的根节点。策略树在RFC 5280中有更详细的讨论。
PolicyNode
的getPolicyQualifiers
方法返回一组PolicyQualifierInfo
对象,每个对象代表此策略适用的相关证书的证书策略扩展中包含的策略限定符。
大多数应用程序不需要检查有效的策略树和策略限定符。它们可以通过在PKIXParameters
中设置与策略相关的参数来实现策略处理目标。但是,有效的策略树可用于更复杂的应用程序,特别是那些处理策略限定符的应用程序。
例子:使用PKIX算法验证证书链(源于官方文档)
示例忽略了大多数异常处理,并假设已经创建了信任锚的证书链和公钥。
//首先,创建一个CertPathValidator对象用于验证,并指定使用PKIX算法
CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
//创建一个TrustAnchor对象,指定最受信任的CA的公钥,它将用作验证证书链的锚,这里使用CA的名称和其公钥(参数pubkey)的形式指定
TrustAnchor anchor = new TrustAnchor("O=xyz,C=us", pubkey, null);
//然后创建一个PKIXParameters对象。这将用于填充PKIX算法所使用的参数。这里我们放入信任锚
PKIXParameters params = new PKIXParameters(Collections.singleton(anchor));
//然后填充验证算法使用的约束或其他参数
//我们启用了explicitPolicyRequired标志并指定了一组初始策略(集合的内容没有显示):
params.setExplicitPolicyRequired(true);
params.setInitialPolicies(policyIds);
//最后就是使用我们创建的参数对象去验证
try{
//假定我们已经有了一个证书链对象certPath
PKIXCertPathValidatorResult result =(PKIXCertPathValidatorResult) cpv.validate(certPath,params);
//获取策略树的根节点
PolicyNode policyTree = result.getPolicyTree();
//获取被验证的主体的公钥,如果验证通过,就可以信任这个证书中的公钥了
PublicKey subjectPublicKey = result.getPublicKey();
} catch (CertPathValidatorException cpve) {//如果验证失败,则打印异常对象中的信息
System.out.println("Validation failure, cert[" + cpve.getIndex() + "] :" + cpve.getMessage());
}
3.2.5.5 PKIXBuilderParameters类
public class PKIXBuilderParameters extends PKIXParameters
这个类(继承自PKIXParameters
类)指定了与CertPathBuilder
类一起使用的一组参数。
PKIXBuilderParameters
对象作为参数传递给实现PKIX
算法的CertPathBuilder
实例的build
方法。所有的使用PKIX
算法的CertPathBuilder
必须返回根据PKIX
证书链验证算法验证过的证书链。
请注意,PKIX CertPathBuilder
用于验证构造路径的机制是实现细节。 例如,一个实现可能会尝试首先使用最少的验证来构建路径,然后使用PKIX
CertPathValidator
的实例对其进行完全验证,而更有效的实现可能会在构建路径时验证更多路径,并如果遇到验证失败或死胡同则回溯到先前的阶段。
创建一个PKIXBuilderParameters对象
创建PKIXBuilderParameters
对象类似于创建PKIXParameters
对象。 但是,在创建PKIXBuilderParameters
对象时,调用者必须在目标或最终实体证书上指定约束。 这些约束应为CertPathBuilder
提供足够的信息以查找目标证书。 约束被指定为CertSelector
对象。 使用以下构造函数之一创建PKIXBuilderParameters
对象:
//keystore中存储的是最受信任的证书集合
public PKIXBuilderParameters(KeyStore keystore, CertSelector targetConstraints)
throws KeyStoreException, InvalidAlgorithmParameterException
//trustAnchors信任锚集合
public PKIXBuilderParameters(Set<TrustAnchor> trustAnchors, CertSelector targetConstraints)
throws InvalidAlgorithmParameterException
设置/获取参数值
PKIXBuilderParameters
类继承了可以在PKIXParameters
类中设置的所有参数(参见PKIXParameters
那一小节或者API文档)。 另外,可以调用setMaxPathLength
方法来限制构建的证书路径中证书的最大数量:
public void setMaxPathLength(int maxPathLength)
maxPathLength
参数指定证书路径中可能存在的非自行发行的中间证书的最大数量。 实现PKIX
算法的CertPathBuilder
实例的路径长度不能超过指定的长度。 如果值为0,则路径只能包含一个证书。 如果值是-1,则路径长度不受限制(即没有最大值)。 默认最大路径长度(如果未指定)为5。此方法可用于防止CertPathBuilder
花费资源和时间来构建可能满足或不满足调用者要求的长路径。
如果路径中的任何CA证书包含“基本约束”扩展,则只要结果是长度较短的证书路径,该扩展的pathLenConstraint
组件的值就会覆盖maxPathLength
参数的值。 还有一个对应的getMaxPathLength
方法来检索此参数:
public int getMaxPathLength()
同样,setCertStores
方法(从PKIXParameters
类继承)通常由CertPathBuilder
的PKIX
实现使用来查找用于路径构造的证书以及用于路径验证的CRL。 这为指定证书和CRL的位置提供了可扩展的机制。
3.2.5.6 PKIXCertPathBuilderResult类
public class PKIXCertPathBuilderResult extends PKIXCertPathValidatorResult implements CertPathBuilderResult
PKIXCertPathBuilderResult
类表示PKIX认证路径构建算法的成功结果,不成功就抛异常。
此类继承了PKIXCertPathValidatorResult
类,并实现了CertPathBuilderResult
接口(官方文档这里有笔误,文档上写的是实现了CertPathBuilder
)。PKIXCertPathBuilderResult
的实例由实现PKIX
算法的CertPathBuilder
对象的构建方法返回。
PKIXCertPathBuilderResult
实例的getCertPath
方法始终返回使用PKIX
证书链验证算法验证过的CertPath
对象。 返回的CertPath
对象不包括可能已用于锚定路径的最受信任的CA证书。 而是使用getTrustAnchor
方法获取最受信任的CA的证书。
例子:使用PKIX
算法构建一个证书链(源于官方文档)
这个例子里省略了很多细节,如一些变量的定义和异常处理等
//首先创建一个支持PKIX算法的CertPathBuilder对象
CertPathBuilder cpb = CertPathBuilder.getInstance("PKIX");
//创建一个PKIXBuilderParameters对象,这用于填充PKIX证书链构建算法的参数
//创建一个对目标证书的约束
X509CertSelector targetConstraints = new X509CertSelector();
targetConstraints.setSubject("CN=alice,O=xyz,C=us");
//使用信任锚对象集合trustAnchors和证书约束targetConstraints实例化一个PKIXBuilderParameters对象
PKIXBuilderParameters params = new PKIXBuilderParameters(trustAnchors, targetConstraints);
//指定CertPathBuilder将用于查找证书和crl的CertStore
//创建一个CollectionCertStoreParameters对象(本地检索),certsAndCrls是一个包含了证书和CRL的集合,证书链将会从这个集合中的证书创建
CollectionCertStoreParameters ccsp = new CollectionCertStoreParameters(certsAndCrls);
//使用参数创建并初始化一个CertStore对象
CertStore store = CertStore.getInstance("Collection", ccsp);
//将CertStore添加到证书链构建参数里,现在这个参数里就有了证书库、信任锚、约束
params.addCertStore(store);
//然后构建证书链
try {
PKIXCertPathBuilderResult result =(PKIXCertPathBuilderResult) cpb.build(params);
CertPath cp = result.getCertPath();
} catch (CertPathBuilderException cpbe) {
System.out.println("build failed: " + cpbe.getMessage());
}
3.2.5.7 PKIXCertPathChecker类
public abstract class PKIXCertPathChecker implements CertPathChecker, Cloneable
PKIXCertPathChecker
类允许用户扩展PKIX
CertPathValidator
或CertPathBuilder
实现。这是一个高级特性,大多数用户不需要理解。但是,任何实现PKIX
服务提供者的人都应该阅读本节。
PKIXCertPathChecker
类是一个抽象类,它对X.509
证书执行一个或多个检查。当需要在运行时动态扩展PKIX
CertPathValidator
或CertPathBuilder
实现时,开发人员应该创建PKIXCertPathChecker
类的具体实现。
如有需要,请自行查看官方文档
附录:OCSP协议支持
3.3、证书管理工具keytool使用手册
keytool说明
keytool命令行工具是一个密钥和证书管理实用程序。它允许用户通过使用数字签名管理自己的公钥/私钥对和相关证书,以便在自我身份验证(用户向其他用户和服务进行身份验证)或数据完整性和身份验证服务中使用。keytool命令还允许用户缓存正在通信的对等节点的公钥(以证书的形式)。
keytool命令还允许用户管理对称加密和解密(数据加密标准)中使用的密钥和密码。它还可以显示其他与安全相关的信息。
keytool命令将密钥和证书存储在密钥存储库中。
命令和选项说明
包括以下命令(可以运行 keytool --help
命令查看):
-certreq
: Generates a certificate request-changealias
: Changes an entry’s alias-delete
: Deletes an entry-exportcert
: Exports certificate-genkeypair
: Generates a key pair-genseckey
: Generates a secret key-gencert
: Generates a certificate from a certificate request-importcert
: Imports a certificate or a certificate chain-importpass
: Imports a password-importkeystore
: Imports one or all entries from another keystore-keypasswd
: Changes the key password of an entry-list
: Lists entries in a keystore-printcert
: Prints the content of a certificate-printcertreq
: Prints the content of a certificate request-printcrl
: Prints the content of a Certificate Revocation List (CRL) file-storepasswd
: Changes the store password of a keystore-showinfo
: Displays security-related information
以下注意事项适用于命令和选项中的描述:
-
所有命令和选项名前面都有连字符(-)
-
一次只能提供一个命令
-
每个命令的选项可以按任意顺序提供
-
有两种选项,一种是单值选项,应该只提供一次。如果一个单值选项被提供多次,则使用最后一个选项的值。另一种类型是多值类型,可以多次提供,并且使用所有值。目前唯一支持的多值选项是用于生成
X.509v3
证书扩展的-ext
选项。 -
所有没有斜体或大括号{}或方括号[]的选项都必须按原样显示。
-
包围选项的大括号{}表示在命令行中未指定该选项时使用默认值。大括号也用于
-v
、-rfc
和-J
选项,表示这些命令只有在命令行上出现时才有意义,它们没有任何默认值。 -
包围选项的方括号[]表示当命令行中没有指定选项时,提示用户输入值。对于
-keypass
选项,如果您没有在命令行上指定该选项,那么keytool命令首先尝试使用密钥存储库密码来恢复私有/秘密密钥。如果此尝试失败,则keytool命令提示您输入私钥/密钥密码。 -
斜体显示的项(选项值)表示必须提供的实际值。例如,下面是
-printcert
命令的格式:keytool -printcert {-file cert_file} {-v}
当您指定一个
-printcert
命令时,将cert_file
替换为实际的文件名,如下所示keytool -printcert -file VScert.cer
-
当选项值包含空白(空格)时,必须用引号包围
命令类型
keytool命令及其选项可以按照它们执行的任务进行分组。
- 用于创建或向密钥存储库添加数据的命令:
-gencert
-genkeypair
-genseckey
-importcert
-importpass
- 从另一个密钥库导入内容的命令:
-importkeystore
- 用于生成证书请求的命令:
-certreq
- 导出数据的命令:
-exportcert
- 显示数据的命令:
-list
-printcert
-printcertreq
-printcrl
- 用于管理密钥存储库的命令:
-storepasswd
-keypasswd
-delete
-changealias
- 用于显示安全相关信息的命令:
-showinfo
命令的使用
可以查看官方文档,或则使用命令keytool -command_name --help
来查看给定命令的选项及其描述。
示例
生成本地数字证书,并存储到指定的密钥库中。使用了一些默认配置,如密钥大小
PS C:\Users\Zhong> keytool -genkeypair -keyalg RSA -alias liu -keystore E:\\liu.keystore 输入密钥库口令:123456
再次输入新口令:123456
您的名字与姓氏是什么?
[Unknown]: liu
您的组织单位名称是什么?
[Unknown]: njust
您的组织名称是什么?
[Unknown]: njust
您所在的城市或区域名称是什么?
[Unknown]: nj
您所在的省/市/自治区名称是什么?
[Unknown]: js
该单位的双字母国家/地区代码是什么?
[Unknown]: CN
CN=liu, OU=njust, O=njust, L=nj, ST=js, C=CN是否正确?
[否]:y
正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 90 天):
CN=liu, OU=njust, O=njust, L=nj, ST=js, C=CN
导出数字证书
PS C:\Users\Zhong> keytool -exportcert -alias liu -keystore E:\\liu.keystore -file E:\\liu.cer -rfc 输入密钥库口令:123456
存储在文件 <E:\\liu.cer> 中的证书
生成的数字证书可以用Windows直接打开
也可以使用-printcert
命令查看证书内容
keytool -printcert -file E:\\liu.cer
3.4 BC库文档第8章:X.509 Certificates and Attribute Certificates (重要)
本章之前的小节讲的JCE的API不提供使用公钥直接生成证书的API,只能使用keytool工具在命令行中生成。而BC库提供了将公钥和一些主体信息转换为公钥证书的API,下面我们就来了解一下。
3.4.1 The X.500 Distinguished Name (X.500专有名称)
证书的基本身份单位是 X.500
专有名称。X.500
中的主要 ASN.1 结构是Name
,我们将看到它用于填充X.509
证书中的身份字段。
专有名称(distinguished name)或 DN
最初是在 OSI 的 X.501 中提出的,用于描述 X.500 目录结构。 目录结构的想法是它会形成一个层次结构,类似于下图中的层次结构。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MkCZg4Zb-1627221125193)(Java加解密API.assets/image-20210725213517487.png)]
DN的ASN1结构如下:
DistinguishedName ::= RDNSequence
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
AttributeTypeAndValue ::= SEQUENCE {
type OBJECT IDENTIFIER,
value ANY }
这个定义包含的内容非常广泛。
您可能见过的最常见的DN是等价的字符串,如下所示:
"CN=www.bouncycastle.org, OU=Bouncy Castle, O=Legions, C=AU"
每个 X=value
值对组成前面的专有名称(DN),是进入 RelativeDistinguishedName
(RDN)的内容,正如 DistinguishedName
的 ASN.1 结构所暗示的那样,DN
的字符串表示只是一个逗号分隔的列表 X=value
值对。
X被转换为对象标识符。X的字符串版本的通用值及其详细信息如下所示:
CN—commonName
: OID “2.5.4.3”,限制为64个字符OU—organizationalUnitName
: OID “2.5.4.10”, 限制为64个字符C—country
: OID “2.5.4.6”, 限制为2个字符L—localityName
: OID “2.5.4.7”,限制为64个字符ST—stateOrProvinceName
: OID “2.5.4.8”,限制为64个字符
在 Bouncy Castle
中,X.500
名称由 X500Name
类表示,该类定义在 org.bouncycastle.asn1.x500
包下,该包中还有X500NameBuilder
类。
JCA
依赖 X500Principal
类来支持 X.500
名称结构。 与 BC
的 X500Name
类不同,X500Principal
只能从字符串或字节编码创建 - 没有构建器(builder)类。出于这个原因,即使你限制自己使用 X500Principal
有时仍然可以更好地使用 X500NameBuilder
来构造一个Name
,然后使用 X500Name.getEncoded()
从字节编码创建一个新的 X500Principal
,如下所示:
X500NameBuilder x500Bldr = new X500NameBuilder();
X500Principal x500Principal = new X500Principal(x500Bldr.build().getEncoded())
也可以将X500Principal
转为X500Name
X500Name x500Name = X500Name.getInstance(x500Principal.getEncoded())
注意:
在可能的情况下,避免使用字符串表示,或者在使用时小心使用它们。
X500Name
类和X500Principal
类都支持将字符串转换为 X.500 Name 对象,但得到的是不同类如何解释输入的问题。您可能会注意到X.500 Names
的第一个问题是有多种方法可以生成一个字符串的表示形式。 部分原因是不同 OID 的简写如何用来表示属性的类型(例如,“CN”实际上是 OID“2.5.4.3”的简写)。
Bouncy Castle
的X500Name
类允许定义本地样式,因此,例如,您可以使用以下方法指定X500Name
应使用RFC 4519
中采用的方法转换字符串。/** * Convert an X500Name to use the IETF style. */ public static X500Name toIETFName(X500Name name){ return X500Name.getInstance(RFC4519Style.INSTANCE, name); }
3.4.2 公钥证书
与 X.500
一样,X.509
本身在其功能方面非常慷慨,因此人们通常采用特定的配置文件来创建和评估证书。 对于 Internet
而言,最流行的是 RFC-5280 [39]
。 虽然配置文件在解释事物的方式方面可能会有所不同,但格式始终是X.509
中描述的格式。
Certificate 和TBSCertificate
在我们开始研究证书的创建之前,有必要快速浏览一下所涉及的 ASN.1
结构,以便对 API
下发生的事情有所了解。 虽然这个世界上很少有什么东西能让开发人员像 ASN.1
模块那样让开发人员感到恐惧,但还是值得深入了解这些概念,因为如果您使用它,调试出错的情况会容易得多。
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [0] Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] Extensions OPTIONAL
-- If present, version MUST be v3 -- }
Version ::= INTEGER { v1(0), v2(1), v3(2)
}
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time
}
Time ::= CHOICE {
utcTime UTCTime,
generalTime GeneralizedTime
}
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING
}
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
要注意的第一个也是最重要的事情是 TBSCertificate
结构包含证书的主体,包括公钥(在 SubjectPublicKeyInfo
中编码)。 第二件事是,存储在 Certificate
中的签名是通过 TBSCertificate
的 DER
编码计算的(在这种情况下,TBS 实际上代表“待签名”)。 DER
或可分辨编码规则的使用在这里很重要,因为这意味着证书在计算其签名哈希时应始终以相同的方式进行计算。
第二个要注意的是有效期,证书只在特定的时间段内有效。 该决定由 CA
做出,将反映一系列考虑因素,包括签名算法的强度、证书的实体(主体)首先应该拥有证书的时间以及公钥的强度 以及它或其关联的私钥可能获得的使用次数。
最后是版本。 很少看到 v2
证书。 通常您只会遇到 v1
和 v3
证书,如果您看到 v1
证书,它通常是自签名的并用作信任锚(根证书)。 v3
版本的证书通常具有扩展名,在日常使用中,它们是我们最常使用的扩展名,例如验证文档或时间戳,或加密密钥。 我们将进一步仔细研究证书中的常见扩展。
正如您所看到的,TBSCertificate
还包含我们需要的身份信息,既可以帮助我们识别颁发者、主题,还包括序列号,以便我们可以分辨出来自我们正在查看的颁发者的哪个证书。 除了方便了解之外,这还使颁发者可以撤销证书,我们将在本章后面讨论这个主题。
3.4.5 创建一个基础的公钥证书
创建公钥证书除了需要公钥外,还需要一些其他的信息,下面我们先提供两个生成这些额外信息的方法:
/**
* 以秒为单位计算日期(适用于 PKIX 配置文件 - RFC 5280)
* 计算未来的某一个时间点
*
* @param hoursInFuture 从现在开始之后的hoursInFuture个小时
* @return a Date set to now + (hoursInFuture * 60 * 60) seconds
*/
public static Date calculateDate(int hoursInFuture){
long secs = System.currentTimeMillis() / 1000;
return new Date((secs + (hoursInFuture * 60 * 60)) * 1000);
}
private static long serialNumberBase = System.currentTimeMillis();
/**
* 使用单调递增的值计算序列号。
*
* @return 一个表示序列中的下一个序列号的BigInteger。
*/
public static BigInteger calculateSerialNumber() {
return BigInteger.valueOf(serialNumberBase++);
}
Bouncy Castle API
会 生成一个 X509CertificateHolder
类, 因为它只是充当证书的载体。 在查看了如何构建基本证书之后,我们将查看将 X509CertificateHolder
转换为 Java 的 X509Certificate
子类对象。
以下示例显示如何生成自签名的X.509 v1 证书。 v1 证书仍被广泛使用,但仅用作信任锚。
/**
* 构建示例自签名 V1 证书以用作信任锚或根证书。
*
* @param keyPair 用于签名和提供公钥的密钥对。
* @param sigAlg 用于签署证书的签名算法。
* @return 持有v1证书的X509CertificateHolder对象
*/
public static X509CertificateHolder createTrustAnchor(KeyPair keyPair, String sigAlg)
throws OperatorCreationException{
//创建一个x.500 Name,表示签署者的信息
X500NameBuilder x500NameBld = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.C, "AU")
.addRDN(BCStyle.ST, "Victoria")
.addRDN(BCStyle.L, "Melbourne")
.addRDN(BCStyle.O, "The Legion of the Bouncy Castle")
.addRDN(BCStyle.CN, "Demo Root Certificate");
X500Name name = x500NameBld.build();
//x509 v1版本证书构建器,注意使用的时X509v1CertificateBuilder类的子类JcaX509v1CertificateBuilder
X509v1CertificateBuilder certBldr = new JcaX509v1CertificateBuilder(
//发布者名称
name,
//序列号
calculateSerialNumber(),
//有效期起始日期
calculateDate(0),
//有效期结束日期
calculateDate(24 * 31),
//证书持有者名称,自签名
name,
//公钥
keyPair.getPublic());
//签名器,自签名,使用自己的私钥
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC")
.build(keyPair.getPrivate());
//传入签名器,签署公钥证书
return certBldr.build(signer);
}
public static void main(String...args){
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC");
KeyPair trustKp = kpGen.generateKeyPair();
X509CertificateHolder trustCert = createTrustAnchor(trustKp, "SHA256withECDSA");
}
3.4.6 将X509CertificateHolder转换为X509Certificate
提供 org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
类以将 X509CertificateHolder
对象转换为常规 JCA X509Certificate
对象。 JcaX509CertificateConverter
类可用于根据 JVM 中当前的提供者程序的优先级生成证书,或者您可以指定特定的提供者程序。 在此示例中,我们使用名称指定了 Bouncy Castle
提供程序。
//使用上面的例子生成X509CertificateHolder对象
X509CertificateHolder certHldr = ...
X509Certificate cert = new JcaX509CertificateConverter()
.setProvider("BC").getCertificate(certHldr);
反过来,只需将 X509Certificate
的编码传递给 X509CertificateHolder
,或者将 X509Certificate
传递给 JcaX509CertificateHolder
,它会为您执行编码步骤。
//一个证书对象
X509Certificate cert = ...
//将其转换为X509CertificateHolder对象
X509CertificateHolder certHldr = new JcaX509CertificateHolder(cert);
X509CertificateHolder类是一个很有用的类,它包含了很多验证他所包含的证书的有效性的方法,如:
判断是否在有效期内:boolean isValidOn(Date date)
判断签名是否有效:boolean isSignatureValid(ContentVerifierProvider verifierProvider)
等
使用CertificateFactory
类进行转换
这里说的CertificateFactory
类就是前面小节讲的JCE的CertificateFactory
类,它也可以被用于BC的专有API。
java.security.cert
包中的 CertificateFactory
类还可用于将 X509CertificateHolder
或许多其他形式的 X.509
证书转换为 Java 的 X509Certificate
对象之一。
CertificateFactory
的generateCertificate()
和generateCertificates()
可以将编码过的(PEM 和BER/DER)证书信息转换为证书对象。而X509CertificateHolder
的getEncoded()
方法可以将它所持有的证书进行BER编码。所以我们可以这样转换:
/**
* 使用 java.security.cert.CertificateFactory 类将 X509CertificateHolder 转换为 X509Certificate 的简单方法。
*/
public static X509Certificate convertX509CertificateHolder(X509CertificateHolder certHolder)
throws GeneralSecurityException, IOException{
CertificateFactory cFact = CertificateFactory.getInstance("X.509", "BC");
return (X509Certificate)cFact.generateCertificate(
new ByteArrayInputStream(certHolder.getEncoded()));
}
3.4.7 创建一个拥有扩展项 的CA证书
X.509 v3
发布的主要创新是引入了证书扩展。 在我们继续之前,最好了解一下如何使用 ASN.1 扩展结构对扩展进行编码。下一节讲各个扩展的意思,这里只讲怎么创建。
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
OBJECT IDENTIFIER extnID
用于标识 extnValue
的内容,critical
用于指示 CA 是否认为此扩展足够重要以至于任何希望使用它所在证书的人都必须理解它,然后 extnValue
是一个 OCTET STRING
保存扩展值实际是什么的 DER
编码。 请注意,由于critical
的DEFAULT
值为false
,如果它为false
,则在对扩展结构进行DER
编码时该值将不存在。
扩展提供了两个好处,它们允许添加有关证书主体的额外信息,例如它们/它可能知道的其他名称,并且它们还使 CA 可以对证书的用途施加一些限制 . 当我们稍后查看 CertificationRequests
(请求证书签发时用)时要记住这一点很重要,虽然可以请求添加扩展,但添加到证书的扩展由 CA 签署证书决定。
虽然 X509CertificateHolder
将扩展表示为对象,但如果您使用 JCA X509Certificate
,则将有一个与扩展关联的特定方法,或者您将需要使用 X509Certificate.getExtensionValue()
来检索扩展值。 请注意,返回的扩展值将是包含扩展值的 DER
编码 OCTET STRING
,而不是扩展值本身。 您可以使用以下方法解开扩展值并获取表示扩展值的内容字节:
/**
* 从 JCA X509Certificate 中扩展项的PID提取扩展的 DER 编码值八位字节。
*
* @param cert 证书
* @param extensionOID 扩展的OID,ASN1 结构里的 OBJECT IDENTIFIER
* @return 扩展中的 DER 编码,如果缺少扩展则为 null。
*/
public static byte[] extractExtensionValue(X509Certificate cert,
ASN1ObjectIdentifier extensionOID){
byte[] octString = cert.getExtensionValue(extensionOID.getId());
if (octString == null){
return null;
}
//这是BC的API
return ASN1OctetString.getInstance(octString).getOctets();
}
考虑以下示例,该示例创建了一个可用于签署其他证书的中间证书。 该方法使用 JcaX509ExtensionUtils
类并创建许多扩展,以提供有关颁发者的额外信息、证书公钥的通用标识符以及有关签署证书的 CA 期望使用证书的内容的一些详细信息。
/**
* 构建可用作 CA 证书的示例 V3 中间证书。
*
* @param signerCert 带有公钥的证书,稍后将用于验证此证书的签名。这是签发者的证书。
* @param signerKey 用于在证书中生成签名的私钥。
* @param sigAlg 签名算法
* @param certKey 要安装在证书中的公钥。
* @param followingCACerts
* @return 包含 V3 证书的 X509CertificateHolder。
*/
public static X509CertificateHolder createIntermediateCertificate(
X509CertificateHolder signerCert, PrivateKey signerKey, String sigAlg,
PublicKey certKey, int followingCACerts)
throws CertIOException,GeneralSecurityException,OperatorCreationException{
X500NameBuilder x500NameBld = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.C, "AU")
.addRDN(BCStyle.ST, "Victoria")
.addRDN(BCStyle.L, "Melbourne")
.addRDN(BCStyle.O, "The Legion of the Bouncy Castle")
.addRDN(BCStyle.CN, "Demo Intermediate Certificate");
X500Name subject = x500NameBld.build();
//注意使用的是v3
X509v3CertificateBuilder certBldr = new JcaX509v3CertificateBuilder(
signerCert.getSubject(),//签发者的名称
calculateSerialNumber(),
calculateDate(0),
calculateDate(24 * 31),
subject,
certKey);
//用于根据给定的信息创建标准的扩展对象
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
//Extension是BC库的API,不是JCE的那个Extension,给证书构建器中添加扩展项
//回顾前面给出的扩展项的ASN.1的结构,需要给的信息有:OID、是否重要和扩展值
certBldr
//指定签发者的证书信息
.addExtension(Extension.authorityKeyIdentifier,//OID
false, //是否重要
extUtils.createAuthorityKeyIdentifier(signerCert))//扩展值
.addExtension(Extension.subjectKeyIdentifier,
false,
extUtils.createSubjectKeyIdentifier(certKey))
//指定该证书是否可以签发其他CA证书
.addExtension(Extension.basicConstraints,
true,
new BasicConstraints(followingCACerts))
//标识此证书的用途
.addExtension(Extension.keyUsage,
true,
new KeyUsage(
KeyUsage.digitalSignature| KeyUsage.keyCertSign | KeyUsage.cRLSign
)
);
//用于签名的签名器
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC")
.build(signerKey);
return certBldr.build(signer);
}
给定信任锚 trustAnchorKey
的 PrivateKey
及其关联的证书 trustCert
,我们可以调用上面的方法来创建单级 CA 证书,如下所示:
//生成要创建的CA的公私钥
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC");
KeyPair caKp = kpGen.generateKeyPair();
//生成单级CA的证书
X509CertificateHolder caCert = createIntermediateCertificate(trustCert,
trustAnchorKey,
"SHA256withECDSA",
caKp.getPublic(),
0);
请注意,该调用传递了 0 作为followingCACerts
的值。 我们学习下面介绍的 BasicConstraints
扩展时,将看到这意味着什么。
3.4.8 常见的证书扩展
这些扩展就是前面Extension
结构中的extnValue
扩展值对应的java对象。
基础扩展(Basic Constraints)
basicConstraints
扩展由常量 Extension.basicConstraints
标识(BC的API),其 OID 值为“2.5.29.19”(id-ce-basicConstraints,BC库的Extension
类中包含了各种扩展的项OID常量值)。 它的 ASN.1 定义如下所示:
BasicConstraints ::= SEQUENCE {
--是否是CA证书,如果是,他可以签署其他证书
cA BOOLEAN DEFAULT FALSE,
--如果是CA证书,它能给签署的证书链长度(CA的)是多少,如果是0,则代表只能签署最终实体证书
pathLenConstraint INTEGER (0..MAX) OPTIONAL
}
BasicConstraints
扩展可帮助您确定是否允许该证书签署其他证书,如果允许,这可以达到什么深度。 因此,例如,如果 cA
为 TRUE
且 pathLenConstraint
为 0,那么就该扩展而言,该证书是允许签署其他证书的(true),但是不允许签署CA证书(0),只能签署最终实体证书(如上例)。
要使用 X509CertificateHolder
类恢复 BasicConstraints
,您可以使用:
BasicConstraints basicConstraints = BasicConstraints.fromExtensions(certHldr.getExtensions());
在 Java 的 X509Certificate
类的情况下,提供了一个方法 X509Certificate.getBasicConstraints()
返回一个 int
,表示 pathLenConstraint
。如果 cA
值为 FALSE
,则 getBasicConstraints()
的返回值为 -1。
权限密钥标识符(Authority Key Identifier)
authorityKeyIdentifier
扩展由常量 Extension.authorityKeyIdentifier
标识,该常量具有 OID
值2.5.29.35
(id-ce-authorityKeyIdentifier
)。 它的ASN.1
定义如下所示:
AuthorityKeyIdentifier ::= SEQUENCE {
keyIdentifier [0] KeyIdentifier OPTIONAL,
authorityCertIssuer [1] GeneralNames OPTIONAL,
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL
}
KeyIdentifier ::= OCTET STRING
此扩展的目的是识别可用于验证证书签名的公钥,或者换句话说,它标识了是哪个主体的公钥证书可以验证此证书。 因此,如果您愿意接受此扩展中标识的公钥(签发者的公钥证书),那么您也可以相信存储在证书中的相关属性的公钥也是有效的。 另一方面,如果您无法理解此扩展或验证证书的签名者,则接受证书中的任何内容更像是一种盲目的行为,而不是基于所使用的签名算法的有效性的行为 .
要使用 X509CertificateHolder
类恢复 AuthorityKeyIdentifier
,您可以使用:
AuthorityKeyIdentifier authorityKeyIdentifier =
AuthorityKeyIdentifier.fromExtensions(certHldr.getExtensions());
JCE
的X509Certificate
没有提供直接获取AuthorityKeyIdentifier
的API,我们可以使用前面定义的extractExtensionValue
方法获取证书中的AuthorityKeyIdentifier
值:
X509Certificate cert = ...
AuthorityKeyIdentifier authorityKeyIdentifier =AuthorityKeyIdentifier.getInstance(
extractExtensionValue(cert, Extension.authorityKeyIdentifier)
);
主体密钥标识符(Subject Key Identifier)
subjectKeyIdentifier
扩展由常量 Extension.subjectKeyIdentifer
标识,该常量具有 OID 值2.5.29.14
(id-ce-subjectKeyIdentifier
)。 它的 ASN.1
定义如下所示:
SubjectKeyIdentifier ::= KeyIdentifier
KeyIdentifier ::= OCTET STRING
subjectKeyIdentifier
只是一个八位字节串,用于为证书包含的公钥提供标识符。 例如,如果您正在寻找特定证书的颁发者的证书,并且该特定证书的 AuthorityKeyIdentifier
设置了 keyIdentifier
字段,则你可以去找到证书中subjectKeyIdentifier
的值是特定证书中AuthorityKeyIdentifier
的keyIdentifier
值。这意味着找到的这个证书就是颁发者的证书。
要使用 X509CertificateHolder
类恢复 SubjectKeyIdentifier
,您可以使用:
SubjectKeyIdentifier subjectKeyIdentifier =
SubjectKeyIdentifier.fromExtensions(certHldr.getExtensions());
JCE
的X509Certificate
没有提供直接获取SubjectKeyIdentifier
的API,我们可以使用前面定义的extractExtensionValue
方法获取证书中的SubjectKeyIdentifier
值:
X509Certificate cert = ...
SubjectKeyIdentifier subjectKeyIdentifier =SubjectKeyIdentifier.getInstance(
extractExtensionValue(cert, Extension.subjectKeyIdentifier)
);
主体备用名称(Subject Alternative Name)
subjectAltName
扩展由常量 Extension.subjectAlternativeName
标识,该常量具有 OID 值2.5.29.17
(id-ce-subjectAltName)。 它的 ASN.1
定义如下所示:
SubjectAltName ::= GeneralNames
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
subjectAltName
用于存储主体名的备用或别名以与证书关联。 如果要将电子邮件地址与证书相关联,严格来说,这是放置它的最佳位置。
要使用 X509CertificateHolder
类恢复 subjectAltName
扩展的 GeneralNames
对象,您可以使用:
GeneralNames subjectAltName = GeneralNames.fromExtensions(certHldr.getExtensions(),
Extension.subjectAlternativeName);
对于 Java 的 X509Certificate
类,提供了 X509Certificate.getSubjectAlternativeNames()
方法,它返回一个不可变的 List
对象集合,这些对象表示可以在该扩展中找到的所有值(麻烦,不建议使用)。
证书颁发者备用名(Issuer Alternative Name)
issuerAltName
扩展由常量Extension.issuerAlternativeName
标识,该常量具有 OID 值2.5.29.18
(id-ce-issuerAltName)。 它的 ASN.1
定义如下所示:
IssuerAltName ::= GeneralNames
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
与 subjectAltName
一样,issuerAltName
用于存储可与证书颁发者关联的备用名称。 与subjectAltName
扩展不同的是,此扩展不能替代TBSCertificate
结构中归档的颁发者的内容。
要使用 X509CertificateHolder
类恢复 issuerAltName
扩展的 GeneralNames
对象,您可以使用:
GeneralNames issuerAltName = GeneralNames.fromExtensions(certHldr.getExtensions(),
Extension.issuerAlternativeName);
对于 Java 的 X509Certificate
类,提供了 X509Certificate.getIssuerAlternativeNames()
方法。
CRL 分发点(CRL Distribution Point)(了解即可)
CRL 分发点扩展提供了一种方法来识别如何获取 CRL 信息。 扩展应该是非关键(non-critical,Extension结构中的critical值)的,并由 OID 值 2.5.29.31
(idce-cRLDistributionPoints) 标识。
扩展的语法描述如下:
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
DistributionPoint ::= SEQUENCE {
distributionPoint [0] DistributionPointName OPTIONAL,
reasons [1] ReasonFlags OPTIONAL,
cRLIssuer [2] GeneralNames OPTIONAL
}
DistributionPointName ::= CHOICE {
fullName [0] GeneralNames,
nameRelativeToCRLIssuer [1] RelativeDistinguishedName
}
ReasonFlags ::= BIT STRING {
unused (0),
keyCompromise (1),
cACompromise (2),
affiliationChanged (3),
superseded (4),
cessationOfOperation (5),
certificateHold (6),
privilegeWithdrawn (7),
aACompromise (8)
}
CRLDistributionPoints
的结构在 BC 库中由 CRLDistPoint
类表示。
您可以使用以下命令从 X509CertificateHolder
恢复 CRL
分发点扩展值:
CRLDistPoint crlDistPoints =CRLDistPoint.getInstance(
certHldr.getExtensions()
.getExtension(
Extension.cRLDistributionPoints
)
.getParsedValue()
);
密钥用途(Key Usage)
keyUsage
扩展由常量 Extension.keyUsage
标识,其 OID 值为 2.5.29.15
(id-ce-keyUsage)。 它的ASN.1
定义如下所示:
--数组代表比特串的位置
KeyUsage ::= BIT STRING {
digitalSignature (0),--验证签名
nonRepudiation (1),--不可否认性
keyEncipherment (2),--密钥加密
dataEncipherment (3),--数据加密
keyAgreement (4),--密钥协商
keyCertSign (5),--给证书签名
cRLSign (6),--撤销列表签名
encipherOnly (7),--仅加密
decipherOnly (8) --仅解密
}
keyUsage
扩展是限制使用包含在证书中的密钥的最通用方法。 哪些比特位必须设置,哪些比特位不能设置,很大程度上取决于使用证书的配置文件以及它的用途。
要使用 X509CertificateHolder
类恢复 KeyUsage
,您可以使用:
KeyUsage keyUsage = KeyUsage.fromExtensions(certHldr.getExtensions());
Java X509Certificate
类使用特定方法 X509Certificate.getKeyUsage()
为该扩展提供特定处理,该方法返回表示 KeyUsage
位串中每一位的布尔值数组。 请注意,在这种情况下,布尔值的顺序反映了它们在 ASN.1 定义中的编号,而不是在 BIT STRING 中设置位的实际顺序。
扩展的密钥用途(Extended Key Usage)
extKeyUsage
扩展由常量 Extension.extendedKeyUsage
标识,其 OID 值为 2.5.29.37
(id-ce-extKeyUsage)。 它的 ASN.1 定义如下所示:
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
KeyPurposeId ::= OBJECT IDENTIFIER
如果此扩展存在,则证书仅用于其中列出的目的之一,除非特殊的 KeyPurposeId anyExtendedKeyUsage
包含在 ExtKeyUsageSyntax
序列中。 通常,此扩展与终端实体证书一起使用,以比密钥使用扩展所允许的更严格的方式将证书锁定到特定目的。RFC 5280 目前为 KeyPurposeId
提供了以下定义:
anyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 }
id-pkix OBJECT IDENTIFIER ::={
iso(1) identified-organization(3) dod(6) internet(1)security(5) mechanisms(5) pkix(7)
}
id-kp OBJECT IDENTIFIER ::= { id-pkix 3 }
id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 }--服务器认证
id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 }--客户端认证
id-kp-codeSigning OBJECT IDENTIFIER ::= { id-kp 3 }--代码签名
id-kp-emailProtection OBJECT IDENTIFIER ::= { id-kp 4 }--邮件保护
id-kp-timeStamping OBJECT IDENTIFIER ::= { id-kp 8 }--签署时间戳
id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 }
要使用 X509CertificateHolder
类恢复 ExtendedKeyUsage
,您可以使用:
ExtendedKeyUsage extKeyUsage = ExtendedKeyUsage.fromExtensions(certHldr.getExtensions());
对于 Java X509Certificate
类,此扩展也由特定方法X509Certificate.getExtendedKeyUsage()
表示,该方法返回表示已在扩展中设置的 OBJECT IDENTIFIER
值的字符串列表。
3.4.9 创建终端实体证书
正如我们前面提到的,终端实体证书代表证书链的末端。
以下示例创建了一个最终实体证书,该证书被授权用于验证数字签名。 请注意,该示例包含一个 BasicConstraints
扩展,但这次扩展值不是使用路径长度创建的,而是使用布尔值 false
来创建的。
/**
* 创建一个用于数字签名的终端实体证书
*
* @param signerCert 带有公钥的证书,稍后将用于验证此证书的签名。即签发者的证书
* @param signerKey 签发者的私钥,用于签发证书
* @param sigAlg 签名算法
* @param certKey 终端实体证书中的公钥
* @return an X509CertificateHolder containing the V3 certificate.
*/
public static X509CertificateHolder createEndEntity(
X509CertificateHolder signerCert,PrivateKey signerKey,String sigAlg, PublicKey certKey)
throws CertIOException, GeneralSecurityException,OperatorCreationException{
X500NameBuilder x500NameBld = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.C, "AU")
.addRDN(BCStyle.ST, "Victoria")
.addRDN(BCStyle.L, "Melbourne")
.addRDN(BCStyle.O, "The Legion of the Bouncy Castle")
.addRDN(BCStyle.CN, "Demo End-Entity Certificate");
//终端实体的名称
X500Name subject = x500NameBld.build();
X509v3CertificateBuilder certBldr = new JcaX509v3CertificateBuilder(
signerCert.getSubject(),
calculateSerialNumber(),
calculateDate(0),
calculateDate(24 * 31),
subject,
certKey
);
//扩展工具类
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
certBldr
.addExtension(//签发者的公钥证书id
Extension.authorityKeyIdentifier,
false,
extUtils.createAuthorityKeyIdentifier(signerCert)
)
.addExtension(//主体的公钥证书id
Extension.subjectKeyIdentifier,
false,
extUtils.createSubjectKeyIdentifier(certKey)
)
.addExtension(//是否可以签发其他证书
Extension.basicConstraints,
true,
new BasicConstraints(false)
)
.addExtension(//只能用于数字签名
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature)
);
//签名器
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC")
.build(signerKey);
return certBldr.build(signer);
}
使用ExtendedKeyUsage
在第二个示例中,我们正在创建一个包含 ExtendedKeyUsage
扩展的终端实体证书。正如我们在前面的 ASN.1
定义中看到的那样,ExtendedKeyUsage
被定义为一个或多个 KeyPurposeId
,其中 KeyPurposeId
是一个对象标识符。
以下示例创建包含 ExtendedKeyUsage
扩展的最终实体证书。
/**
* 创建与特定密钥用途相关联的特殊用途终端实体证书。
*
* @param signerCert certificate carrying the public key that will later
* be used to verify this certificate's signature.
* @param signerKey private key used to generate the signature in the
* certificate.
* @param sigAlg the signature algorithm to sign the certificate with.
* @param certKey public key to be installed in the certificate.
* @param keyPurpose 要与此证书的公钥关联的特定 KeyPurposeId。
* @return an X509CertificateHolder containing the V3 certificate.
*/
public static X509CertificateHolder createSpecialPurposeEndEntity(
X509CertificateHolder signerCert, PrivateKey signerKey,
String sigAlg, PublicKey certKey, KeyPurposeId keyPurpose)
throws OperatorCreationException, CertIOException,GeneralSecurityException{
X500NameBuilder x500NameBld = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.C, "AU")
.addRDN(BCStyle.ST, "Victoria")
.addRDN(BCStyle.L, "Melbourne")
.addRDN(BCStyle.O, "The Legion of the Bouncy Castle")
.addRDN(BCStyle.CN, "Demo End-Entity Certificate");
X500Name subject = x500NameBld.build();
X509v3CertificateBuilder certBldr = new JcaX509v3CertificateBuilder(
signerCert.getSubject(),
calculateSerialNumber(),
calculateDate(0),
calculateDate(24 * 31),
subject,
certKey
);
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
certBldr
.addExtension(
Extension.authorityKeyIdentifier,
false,
extUtils.createAuthorityKeyIdentifier(signerCert)
)
.addExtension(
Extension.subjectKeyIdentifier,
false,
extUtils.createSubjectKeyIdentifier(certKey)
)
.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(false)
)
.addExtension(//添加扩展的用途
Extension.extendedKeyUsage,
true,
new ExtendedKeyUsage(keyPurpose)
);
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC")
.build(signerKey);
return certBldr.build(signer);
}
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC");
KeyPair specEEKp = kpGen.generateKeyPair();
X509CertificateHolder specEECert =createSpecialPurposeEndEntity(
caCert,
caPrivKey,
"SHA256withECDSA",
specEEKp.getPublic(),
KeyPurposeId.id_kp_timeStamping//专门用于签署时间戳的证书
);
3.4.10 属性证书(Attribute Certificates)(了解)
属性证书是一种轻量级的数字证书,这种数字证书不包含公钥信息,只包含证书所有人ID、发行证书ID、签名算法、有效期、属性等信息。一般的属性证书的有效期均比较短,这样可以避免公钥证书在处理CRL时的问题。如果属性证书的有效期很短,到了有效期的日期,证书将会自动失效,从而避免了公钥证书在撤消时的种种弊端。属性一般由属性类别和属性值组成,也可以是多个属性类别和属性值的组合。**这种证书利用属性来定义每个证书持有者的权限、角色等信息。**从而可以解决PKI中所面临的问题,对信任进行一定程度的管理。
属性证书的特点 :
- 身份和权限的分离,适应了互联网应用的特点
- 不同的用户具备不同的访问权限
- 需要分别为用户进行访问授权
- 同一用户的权限信息可能经常发生改变
- 用户的授权方和权限的验证方分离
相关的基础设施:权限管理基础设施(Privilege Management Infrastructure,简称PMI)。
属性证书的结构定义如下:
AttributeCertificate ::= SEQUENCE {
acinfo AttributeCertificateInfo,--属性信息
signatureAlgorithm AlgorithmIdentifier,--签名算法
signatureValue BIT STRING --签名值
}
AttributeCertificateInfo ::= SEQUENCE {
version AttCertVersion, -- version is v2
holder Holder,
issuer AttCertIssuer, --签发者
signature AlgorithmIdentifier,
serialNumber CertificateSerialNumber,
attrCertValidityPeriod AttCertValidityPeriod,
attributes SEQUENCE OF Attribute,
issuerUniqueID UniqueIdentifier OPTIONAL,
extensions Extensions OPTIONAL
}
AttCertVersion ::= INTEGER { v2(1) }
AttCertValidityPeriod ::= SEQUENCE {
notBeforeTime GeneralizedTime,
notAfterTime GeneralizedTime
}
您可以看到外层的基本结构如何遵循与 X.509 证书相同的模式,其中 AttributeCertificateInfo
是 X.509
中使用的 TBSCertificate
结构的本地等效项。 甚至 AttributeCertificateInfo
也有几个字段,归根结底,它们与 TBSCerficate 相同。
但是有两个很大的区别。 AttributeCertificateInfo
结构不是一个主体(subject),而是一个持有者(holder),它描述了如何匹配颁发属性证书的公钥证书。 其次,不是公钥,AttributeCertificateInfo
结构有一个属性字段(attributes),其中包含颁发者希望提供给颁发属性证书的公钥证书持有者的附加属性。
构建属性证书
以下示例显示了使用 RFC 5755 中定义的 RoleSyntax
构建仅包含用户角色的 URI
的简单属性证书。
public static X509AttributeCertificateHolder createAttributeCertificate(
X509CertificateHolder issuerCert,
PrivateKey issuerKey,
String sigAlg,
X509CertificateHolder holderCert,
String holderRoleUri)throws OperatorCreationException{
X509v2AttributeCertificateBuilder acBldr = new X509v2AttributeCertificateBuilder(
new AttributeCertificateHolder(holderCert),
new AttributeCertificateIssuer(issuerCert.getSubject()),
calculateSerialNumber(),
calculateDate(0),
calculateDate(24 * 7)
);
GeneralName roleName = new GeneralName(
GeneralName.uniformResourceIdentifier,
holderRoleUri
);
acBldr.addAttribute(
X509AttributeIdentifiers.id_at_role,
new RoleSyntax(roleName)
);
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC")
.build(issuerKey);
return acBldr.build(signer);
}
3.5 BC库文档第9章:证书撤销和证书路径(重要)
有时,颁发者需要从证书中撤回其签名。 此过程也称为证书撤销,并且有静态和在线方法来支持此过程。
有时,有时颁发证书的一方可能会尝试将其用于他们不应该做的事情。 为了防止这种情况发生,有一些已被标准化的算法用于进行证书路径验证。 这些算法可用于检查签发我们已获得的最终实体证书所涉及的所有证书的来源,并确认最终实体证书“适合用途”。
3.5.1 证书撤销列表 Certificate Revocation Lists (CRLs)
证书撤销列表可以主动声明一个已发布的证书提前作废,而不是被动地等到它过期。
它的ASN.1结构如下:
CertificateList ::= SEQUENCE {
tbsCertList TBSCertList,--证书列表
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING
}
TBSCertList ::= SEQUENCE {
version Version OPTIONAL,
-- if present, MUST be v2
signature AlgorithmIdentifier,
issuer Name,
thisUpdate Time,
nextUpdate Time OPTIONAL,
revokedCertificates SEQUENCE OF SEQUENCE {
userCertificate CertificateSerialNumber,
revocationDate Time,
crlEntryExtensions Extensions OPTIONAL
-- if present, version MUST be v2
} OPTIONAL,
crlExtensions [0] EXPLICIT Extensions OPTIONAL
-- if present, version MUST be v2
}
从结构中可以看出,CRL 的目的是从签署他们的人那里获得他们的权力,并包含一些发行人的详细信息、撤销列表、有关 CRL 何时发布的详细信息,以及一些扩展。
常见CRL扩展
- 权限密钥标识符(Authority Key Identifier):权限密钥标识符扩展提供了一种识别公钥的方法,该公钥可用于验证 CRL 上的签名。意思与公钥证书中的同名扩展相同。
- 颁发者备用名称(Issuer Alternative Name):颁发者备用名称扩展是一个非重要( non-critical)扩展,它允许附加身份与 CRL 相关联。 该扩展与证书的等效扩展具有相同的语法。
- CRL Number:CRL Number也是非关键的,用于提供唯一标识符,指示此 CRL 是否取代另一个 CRL。一致的 CRL 发布者应该随着每个新 CRL 的发布增加 CRLNumber 值,以便具有更高编号的 CRL 始终代表 CRL 的更新版本。 请务必注意以下事实:当 CRL 是增量 CRL 时,值 CRLNumber 必须与增量对应的 CRL 共享相同的编号序列。
Common CRL Entry Extensions
从 TBSCertList 的定义中可以看出,revokedCertificates 字段也有用于可选扩展的空间。
-
CRLReason:顾名思义,CRLRreason 扩展表明证书首先在 CRL 中结束的原因。 如果不存在 CLRReason 扩展,则应假设原因代码为 0,如未指定,RFC 5280 还认为,如果未指定原因,则应不存在扩展,并且仅在需要指明其他原因时才使用该扩展。
CRLReason ::= ENUMERATED { unspecified (0), keyCompromise (1), cACompromise (2), affiliationChanged (3), superseded (4), cessationOfOperation (5), certificateHold (6), -- value 7 is not used removeFromCRL (8), privilegeWithdrawn (9), aACompromise (10) }
-
InvalidityDate:InvalidityDate 扩展用于提供知道或怀疑证书无效的确切日期。 当实际失效时间在撤销日期之前时,此扩展不需要回溯 CRL 条目的日期。
-
CertificateIssuer:CertificateIssuer 扩展用于指示证书的真正颁发者是谁。
创建一个新的CRL
/**
* 创建了一个只包含一个需要撤销的证书的CRL
*
* @param caKey 用于签名该CRL的私钥.
* @param sigAlg 签名算法名称
* @param caCert 签名私钥对应的公钥
* @param certToRevoke 被撤销的证书
* @return 代表CA所撤销证书的撤销列表包装类X509CRLHolder
*/
public X509CRLHolder createCRL(
PrivateKey caKey,
String sigAlg,
X509CertificateHolder caCert,
X509CertificateHolder certToRevoke)
throws IOException, GeneralSecurityException, OperatorCreationException{
X509v2CRLBuilder crlGen = new X509v2CRLBuilder(caCert.getSubject(),
calculateDate(0));
//指定CRL的生命周期为7天
crlGen.setNextUpdate(calculateDate(24 * 7));
//添加撤销
ExtensionsGenerator extGen = new ExtensionsGenerator();
//指定撤销原因
CRLReason crlReason = CRLReason.lookup(CRLReason.privilegeWithdrawn);
extGen.addExtension(Extension.reasonCode, false, crlReason);
//添加到撤销列表
crlGen.addCRLEntry(
//只记录被撤销证书的序列号
certToRevoke.getSerialNumber(),
new Date(),
extGen.generate()
);
// add extensions to CRL
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
crlGen.addExtension(
Extension.authorityKeyIdentifier,
false,
extUtils.createAuthorityKeyIdentifier(caCert)
);
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC").build(caKey);
return crlGen.build(signer);
}
我们的第一步是为 CRL 创建一个构建器,即 X509v2CRLBuilder
类,在这种情况下,还调用setNextUpdate()
方法,指定 CRL 的生命周期为 7 天。 之后我们添加一个撤销,提供一个带有 CRLreason
扩展的 CRL 条目。
3.5.2 将X509CRLHolder转为X509CRL
X509CRL
是JCE标准API
提供 org.bouncycastle.cert.jcajce.JcaX509CRLConverter
类以将 X509CRLHolder
对象转换为常规 JCA X509CRL
对象。
X509CRLHolder crlHldr = ...
X509CRL crl = new JcaX509CRLConverter().setProvider("BC").getCRL(crlHldr);
也可以用JCE的CertificateFactory
来转
public static X509CRL convertX509CRLHolder(
X509CertificateHolder crlHolder)
throws GeneralSecurityException, IOException {
CertificateFactory cFact = CertificateFactory.getInstance("X.509", "BC");
return (X509CRL) cFact.generateCRL(new ByteArrayInputStream(crlHolder.getEncoded()));
}
反之:
X509CRL crl = ...
X509CRLHolder crlHldr = new JcaX509CRLHolder(crl);
3.5.3 向一个已有的CRL中添加新的证书
以下示例显示了如何获取现有 CRL 并对其进行更新。
/**
* @param caKey 用于签名这个CRL的CA的私钥
* @param sigAlg 签名算法
* @param caCert 签名私钥对应的CA的公钥证书
* @param previousCaCRL 旧的CRL
* @param certToRevoke 要新添加的需要撤销的证书
* @return an X509CRLHolder representing the updated revocation list for the
* CA.
*/
public X509CRLHolder updateCRL(
PrivateKey caKey,
String sigAlg,
X509CertificateHolder caCert,
X509CRLHolder previousCaCRL,
X509CertificateHolder certToRevoke)
throws IOException, GeneralSecurityException, OperatorCreationException {
//calculateDate方法是前面定义的方法
X509v2CRLBuilder crlGen = new X509v2CRLBuilder(caCert.getIssuer(),calculateDate(0));
//设置有效期
crlGen.setNextUpdate(calculateDate(24 * 7));
// 添加新的撤销证书
//生成撤销的原因
ExtensionsGenerator extGen = new ExtensionsGenerator();
CRLReason crlReason = CRLReason.lookup(CRLReason.privilegeWithdrawn);
extGen.addExtension(Extension.reasonCode, false, crlReason);
//添加证书
crlGen.addCRLEntry(certToRevoke.getSerialNumber(),new Date(), extGen.generate());
// 将之前的CRL也添加上
crlGen.addCRL(previousCaCRL);
// 添加扩展
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
crlGen.addExtension(Extension.authorityKeyIdentifier, false,
extUtils.createAuthorityKeyIdentifier(caCert));
//签名
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC").build(caKey);
return crlGen.build(signer);
}
3.5.4 在线证书状态协议(Online Certificate Status Protocol OCSP)(重要)
如果您希望证书的周转量很大,那么 CRL 会存在一些明显的问题。 不断发布更新(尤其是在一个狭窄的时间窗口内)并确保所有相关方都收到更新可能很快变得难以管理。 目前在 RFC 6960 [46] 中定义的在线证书状态协议 (OCSP) 就是为了解决这个问题而开发的。
基本协议如下图:
服务器提供证书,OCSP 响应者以对客户端的有效性保证进行响应
OCSP 被描述为客户端发送请求,然后 CA 负责通过响应者回复这些请求。 来自 CA 的响应是经过签名的,并且可以具有与其关联的生命周期 - 就像 CRL 具有与其关联的“next update
”一样。
在 Bouncy Castle 的上下文中,支持 OCSP 请求和响应生成。 用于处理 OCSP 的高级类位于 bcpkix 发行版中的 org.bouncycastle.ocsp
包下。 组成 ASN.1
协议元素的低级类可以在org.bouncycastle.asn1.ocsp
中找到。
生成OCSP请求
/**
* 从带有 nonce 扩展的 issuerCert 表示的颁发者生成关于证书序列号的 OCSP 请求。
*
* @param issuerCert 我们要检查的证书颁发者的证书。
* @param serialNumber 我们要检查的证书的序列号。
* @return an OCSP request.
*/
public static OCSPReq generateOCSPRequest(
X509CertificateHolder issuerCert, BigInteger serialNumber)
throws OCSPException, OperatorCreationException {
DigestCalculatorProvider digCalcProv = new JcaDigestCalculatorProviderBuilder()
.setProvider("BC").build();
// 为我们正在寻找的证书生成 id
CertificateID id = new CertificateID(digCalcProv.get(CertificateID.HASH_SHA1),
issuerCert, serialNumber);
// 使用 nonce 生成基本请求
OCSPReqBuilder bldr = new OCSPReqBuilder();
bldr.addRequest(id);
// 为 nonce 扩展创建详细信息 - 仅示例!
BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis());
bldr.setRequestExtensions(new Extensions(
new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce,
true, new DEROctetString(nonce.toByteArray()))));
return bldr.build();
}
3.6 BC库第12章: 证书请求和证书管理
3.6.1 PKCS #10 Certification Requests
在 Bouncy Castle 中,您可以使用 PKCS10CertificationRequestBuilder
和 PKCS10CertificationRequest
类(可在 org.bouncycastle.pkcs 包中找到)或从它们扩展的类(例如 JcaPKCS10CertificationRequestBuilder
和 JcaPKCS10CertificationRequest
)创建和处理 PKCS #10 证书请求。
在最基本的层面上,PKCS #10 请求只是一个结构的签名,其中包含您希望与认证请求中的公钥相关联的主体名称。
CertificationRequestInfo ::= SEQUENCE {
version INTEGER { v1(0) } (v1,...),--版本号
subject Name,--主体名称
subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }},--公钥信息
attributes [0] Attributes{{ CRIAttributes }}--属性
}
CertificationRequest ::= SEQUENCE {
certificationRequestInfo CertificationRequestInfo,--请求信息
signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }},--签名算法
signature BIT STRING--签名值
}
/**
* Create a basic PKCS#10 request.
*
* @param keyPair 认证请求所针对的密钥对。
* @param sigAlg 签名算法
* @return 承载一个请求的对象
* @throws OperatorCreationException 如果私钥不适合所选的签名算法。
*/
public static PKCS10CertificationRequest createPKCS10(
KeyPair keyPair, String sigAlg)
throws OperatorCreationException {
//创建一个唯一名称
X500NameBuilder x500NameBld = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.C, "AU")
.addRDN(BCStyle.ST, "Victoria")
.addRDN(BCStyle.L, "Melbourne")
.addRDN(BCStyle.O, "The Legion of the Bouncy Castle");
X500Name subject = x500NameBld.build();
//创建请求
PKCS10CertificationRequestBuilder requestBuilder
= new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic());
//签名器
ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
.setProvider("BC").build(keyPair.getPrivate());
return requestBuilder.build(signer);
}
在 CA 端,您需要能够验证请求中的证书。 以下方法在 JCA 下执行此操作:
/**
* Simple method to check the signature on a PKCS#10 certification test with
* a public key.
*
* @param request the encoding of the PKCS#10 request of interest.
* @return true if the public key verifies the signature, false otherwise.
* @throws OperatorCreationException in case the public key is unsuitable
* @throws PKCSException if the PKCS#10 request cannot be processed.
*/
public static boolean isValidPKCS10Request(byte[] request)
throws OperatorCreationException, PKCSException,GeneralSecurityException, IOException{
JcaPKCS10CertificationRequest jcaRequest =
new JcaPKCS10CertificationRequest(request).setProvider("BC");
PublicKey key = jcaRequest.getPublicKey();
//验证器
ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder()
.setProvider("BC").build(key);
return jcaRequest.isSignatureValid(verifierProvider);
}
四、Java安全套接字扩展 Java Secure Socket Extension (JSSE)
警告:官方文档中这部分内容非常枯燥,代码示例全部集中在后面一个小结中,前面全是描述类和接口的信息,先了解核心的类和接口,再去学习代码示例。更多的代码示例见5.4节
Java安全套接字扩展(JSSE
)支持安全的Internet通信。它为TLS
(即 安全传输层协议 ,安全的TCP
协议)和DTLS
(即 数据包传输层安全性协议,安全的UDP
协议)协议的Java版本提供了一个框架和实现,包括数据加密、服务器身份验证、消息完整性和可选的客户身份验证功能。
4.1 JSSE基本介绍
网上传输的数据很容易被非预期终端(黑客等)截获。当这些数据包含隐私数据时(如密码),则需要保护数据安全地发送到正确的终端(隐私保护和身份认证)。同样也需要保证数据传输的过程中有没有被修改/替换(完整性验证)。安全传输层(TLS
)协议被设计用来帮助保护数据在网络上传输时的隐私和完整性。
Java安全套接字扩展(JSSE
)支持安全的Internet通信。它为TLS
协议的Java版本提供了一个框架和实现,包括数据加密、服务器身份验证、消息完整性和可选的客户身份验证功能。使用JSSE
,开发人员可以通过TCP/IP
在客户机和运行任何应用层协议(如HTTP、Telnet或FTP)的服务器之间提供数据的安全通道。
通过抽象复杂的底层安全算法和握手机制,JSSE
最小化了开发安全应用程序时出现安全漏洞的风险。此外,它作为一个可集成模块,开发人员可以直接集成到他们的应用程序中,从而简化了应用程序开发。
JSSE
提供了API
框架和该API
的实现。通过 java.security
和java.net
包中定义的API
,JSSE API
补充了Java定义的核心网络和加密服务。这些API
提供了扩展的网络套接字类、信任管理器、密钥管理器、SSL
上下文和用于封装套接字创建行为的套接字工厂框架。因为SSLSocket
类基于阻塞I/O
模型,所以Java开发工具包(JDK)包括一个非阻塞的SSLEngine
类,以使程序能够选择自己的I/O方法。
JSSE API支持以下安全协议:
- DTLS: versions 1.0 and 1.2
- TLS: version 1.0, 1.1, 1.2, and 1.3
- SSL (Secure Socket Layer): version 3.0
这些安全协议封装了一个普通的双向流套接字,JSSE API
增加了对身份验证、加密和完整性保护的透明支持。
JSSE
是Java SE平台的一个安全组件,它与JCA
有相同的设计原则,即安全提供者(Provider)实现他要提供的安全服务引擎类。这个与加密相关的安全组件框架允许它们具有实现独立性,并且在可能的情况下,具有算法独立性。JSSE
使用JCA
框架定义的安全服务提供者(Cryptographic Service Providers CSP)。
JSSE API
被设计为允许其他SSL/TLS/DTLS
协议和公钥基础设施(PKI
)实现无缝插入。开发人员还可以提供替代逻辑来确定是否应该信任远程主机,或者应该向远程主机发送什么身份验证密钥材料。
4.1.1 特性
-
作为JDK的标准组件
-
可扩展的、基于提供商的架构
-
100%纯Java实现
-
为
TLS/DTLS
提供API
支持 -
提供
SSL 3.0、TLS
(版本1.0、1.1、1.2和1.3)和DTLS
(版本1.0和1.2)的实现 -
包括可以实例化以创建安全通道的类(SSLSocket, SSLServerSocket和SSLEngine)
-
支持加密套件协商,这是
TLS/DTLS
握手的一部分,用于发起或验证安全通信 -
支持客户端和服务器身份验证,这是正常的T
LS/DTLS
握手的一部分 -
提供了对
TLS
协议封装的HTTP的支持,允许访问数据,如使用HTTPS
的web页面 -
提供服务器会话(session)管理
api
来管理内存驻留的SSL
会话 -
支持证书状态请求扩展(
OCSP stapling
),节省了客户端证书验证的往返和资源 -
提供了对服务端名称表示(Server Name Indication SNI) 扩展的支持,它扩展了
TLS/DTLS
协议来指示客户端在握手过程中试图连接到的服务器名 -
在握手过程中支持端点识别,防止中间人攻击
-
提供了对加密算法约束的支持,提供了对
JSSE
协商算法的细粒度控制
4.1.2 JSSE标准API
JSSE
标准API
,在javax.net和 javax.net.ssl包中定义,提供了:
- 为客户端和服务器端应用量身定制的安全套接字。
- 一个用于生产和消费
TLS/DTLS
数据流的非阻塞引擎(SSLEngine
)。 - 用于创建
Socket
、Server Socket
、SSL Socket
和SSL Server Socket
的工厂。通过使用套接字工厂,您可以封装套接字的创建和配置行为。 - 一个表示安全套接字上下文的类,它充当安全套接字工厂和安全套接字引擎的工厂。
- 密钥和信任管理器接口(包括特定于
X.509
的密钥和可信证书管理器),以及可用于创建它们的工厂。 - 用于安全
HTTP URL
连接(HTTPS
)的类。
4.1.3 SunJSSE 提供者
Java SE的Oracle实现包括一个名为SunJSSE的提供程序,它是预先安装和预先注册在JCA中的。此提供程序提供以下加密服务:
- 实现
SSL 3.0
、TLS
(版本1.0、1.1、1.2和1.3)和DTLS
(版本1.0和1.2)安全协议。 - 最常见的
TLS
和DTLS
加密套件的实现。该实现包括身份验证、密钥协商、加密和完整性保护。 - 一个基于
X.509
的密钥管理器的实现,它从标准的JCA
密钥存储库中选择合适的身份验证密钥。 - 基于
X.509
的可信证书管理器的实现,该管理器实现了证书链验证的规则。
4.2 JSSE的类和接口
为了安全地通信,连接的两端必须启用SSL
。在JSSE API
中,连接的端点类是SSLSocket
和SSLEngine
。在下图中,用于创建SSLSocket
和SSLEngine
的主要类按逻辑顺序排列。
SSLSocket
由SSLSocketFactory
或由入站连接的SSLServerSocket
创建。SSLServerSocket
由SSLServerSocketFactory
创建。SSLSocketFactory
和SSLServerSocketFactory
对象都是由SSLContext
创建的。SSLEngine
是由SSLContext
直接创建的,并依赖于应用程序来处理所有I/O。
提示:当使用原始的
SSLSocket
或SSLEngine
类时,您应该总是在发送任何数据之前检查对等方的数字证书。JDK 7以后,端点鉴别/认证过程可以在SSL/TLS
握手过程中处理。参见SSLParameters.setEndpointIdentificationAlgorithm方法。例如,URL中的主机名应该与对等端证书中的主机名匹配。如果没有验证主机名,应用程序可能会被URL欺骗利用。
4.2.0 一个简单的代码示例
下面的代码来自于官方文档:将一个不安全的Socket程序改造成安全的Socket程序
非安全的套接字程序我们一般这么写
//服务端
int port = 8888;
ServerSocket s=null;
try {
//创建服务器套接字
s = new ServerSocket(port);
//接收连接请求
Socket accept = s.accept();
//获取网络输入输出流
OutputStream out = accept.getOutputStream();
InputStream in = accept.getInputStream();
//.....剩下的步骤
} catch (IOException e) {
e.printStackTrace();
}
//客户端
int port = 8888;
String host = "localhost";
try {
//连接服务器
Socket s = new Socket(host, port);
//获取网络输入输出流
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
//...剩下的步骤
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
简单的安全套接字程序我们这么写
//服务器端
int port = 8888;
SSLServerSocket s=null;
try {
//创建SSL安全服务端套接字工厂
SSLServerSocketFactory sslSrvFact = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
//创建服务器套接字
s = (SSLServerSocket)sslSrvFact.createServerSocket(port);
//接收连接请求
SSLSocket accept = (SSLSocket)s.accept();
//获取网络输入输出流
OutputStream out = accept.getOutputStream();
InputStream in = accept.getInputStream();
//.....剩下的步骤
} catch (IOException e) {
e.printStackTrace();
}
//客户端
int port = 8888;
String host = "localhost";
try {
//创建客户端安全套接字工厂
SSLSocketFactory sslFact = (SSLSocketFactory)SSLSocketFactory.getDefault();
//连接服务器,创建安全套接字
SSLSocket s = (SSLSocket)sslFact.createSocket(host, port);
//获取网络输入输出流
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
//...剩下的步骤
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
4.2.1 SocketFactory 和ServerSocketFactory 类
抽象类javax.net.SocketFactory
用于创建套接字。该类的子类是创建套接字类(Socket)的特定子类的工厂,从而为增加公共套接字层功能提供了一个通用框架。他提供了一个静态方法getDefault()
来返回系统环境的默认套接字工厂的副本。请参阅SSLSocketFactory和SSLServerSocketFactory类。
抽象类javax.net.ServerSocketFactory
类似于SocketFactory
类,但专门用于创建服务器套接字。他提供了一个静态方法getDefault()
来返回系统环境的默认套接字工厂的副本。
套接字工厂是一种简单的方法,可以捕获与正在构造的套接字相关的各种策略,以一种不需要对请求套接字的代码进行特殊配置的方式生成此类套接字:
- 由于工厂和套接字的多态性,不同种类的套接字可以通过传递不同种类的工厂,来被相同的应用程序代码使用。
- 返回给应用程序的套接字可以是
java.net.Socket
(或javax.net.ssl.SSLSocket
,它继承自Socket
类)的子类,因此它们可以直接公开用于压缩、安全性、记录标记、统计数据收集或防火墙隧道等特性的新api
。
4.2.2 SSLSocketFactory 和 SSLServerSocketFactory 类
javax.net.ssl.SSLSocketFactory
类充当创建安全套接字的工厂。这个类是javax.net.SocketFactory
的抽象子类。
安全套接字工厂封装了创建和初始配置安全套接字的细节。这包括认证密钥、对等证书验证、启用的密码套件等。
javax.net.ssl.SSLServerSocketFactory
类似于SSLSocketFactory
类,但专门用于创建服务器套接字。
获取一个SSLSocketFactory实例
以下三种方法可以获取SSLSocketFactory实例:
- 调用
SSLSocketFactory.getDefault()
静态方法来获取默认的工厂 - 接收一个作为
API
参数的工厂。即,代码必须创建套接字但不关心配置套接字的细节,它可以使用一个带有SSLSocketFactory
类型参数的方法来配置套接字,客户端可以调用这个方法来指定在创建套接字时使用哪个SSLSocketFactory
(如:javax.net.ssl.HttpsURLConnection
)。 - 构造一个具有特定配置行为的新工厂。使用
SSLContext
配置一个新的工厂。
默认工厂通常被配置为只支持服务器身份验证,这样默认工厂创建的套接字就不会像普通TCP套接字那样泄漏关于客户机的任何信息。
许多创建和使用套接字的类不需要知道套接字创建行为的细节。通过作为参数传入的套接字工厂创建套接字是隔离套接字配置细节的好方法,并增加了创建和使用套接字的类的可重用性。
您可以通过实现自己的套接字工厂子类或使用另一个充当套接字工厂工厂的类来创建新的套接字工厂实例。
4.2.3 SSLSocket 和 SSLServerSocket 类
javax.net.ssl.SSLSocket
类是标准java.net.Socket
类的子类。它支持所有的标准套接字方法,并添加了特定于安全套接字的方法。此类的实例封装了SSLContext
。
javax.net.ssl.SSLServerSocket
类类似于SSLSocket
类,但专门用于创建服务器套接字。
为了防止对等欺骗,您应该始终验证提交给SSLSocket
的证书。
获取SSLSocket实例
以下方法可以用来获取SSLSocket
实例
- 可以通过
SSLSocketFactory
的实例的一个createSocket
方法来创建SSLSocket
。 - 可以通过
SSLServerSocket
类的accept
方法创建SSLSocket
。
密码套件选择和远程实体验证
SSL/TLS
协议定义了一系列特定的步骤来确保受保护的连接的安全性。然而,密码套件的选择直接影响到连接所享有的安全类型。例如,如果选择了匿名密码套件,那么应用程序就无法验证远程对等体的身份。如果选择了没有加密的套件,则数据的隐私将无法得到保护。此外,SSL/TLS
协议没有指定接收到的证书必须与对等端可能期望发送的证书匹配。如果连接以某种方式重定向到流氓对等点,但根据当前的信任材料,流氓的证书是可以接受的,那么该连接将被认为是有效的。
当使用原始的SSLSocket
和SSLEngine
类时,你应该总是在发送任何数据之前检查对等方的证书。SSLSocket
和SSLEngine
类不会自动验证URL中的主机名与对等方证书中的主机名是否匹配。如果没有验证主机名,应用程序可能会被URL欺骗利用。JDK 7之后,端点识别/验证过程可以在SSL/TLS
握手过程中处理。见 SSLParameters.getEndpointIdentificationAlgorithm方法。
像HTTPS
(通过TLS
传输的HTTP)这样的协议也需要主机名验证。从JDK 7开始,默认情况下,HttpsURLConnection
的握手过程中强制执行HTTPS
端点鉴别。见 SSLParameters.getEndpointIdentificationAlgorithm方法。另外,应用程序可以使用HostnameVerifier
接口来覆盖默认的HTTPS
主机名规则。
4.2.4 SSLEngine类
TLS/DTLS
正变得越来越流行。它被广泛应用于各种各样的计算平台和设备上。随着这种流行,需要使用具有不同I/O和线程模型的TLS/DTLS
来满足应用程序的性能、可伸缩性、内存占用和其他需求。需要在阻塞和非阻塞I/O通道、异步I/O、任意输入和输出流以及字节缓冲区中使用TLS/DTLS
。需要在高度可伸缩、性能关键的环境中使用它,需要管理数千个网络连接。
使用Java SE中的SSLEngine
类对I/O
传输机制进行抽象,允许应用程序以一种传输独立的方式使用TLS/DTLS
协议,从而使应用程序开发人员可以自由地选择最能满足其需求的传输和计算模型。这种抽象不仅允许应用程序使用非阻塞I/O通道和其他I/O模型,它还可以容纳不同的线程模型。这将有效地将I/O和线程决策留给应用程序开发人员。由于这种灵活性,应用程序开发人员必须管理I/O
和线程,并对TLS/DTLS
协议有一定的了解。因此,这个抽象是一个高级API
,初学者应该使用SSLSocket
。
核心类是javax.net.ssl.SSLEngine
。它封装了TLS/DTLS
状态机,并在SSLEngine
类的用户提供的入站和出站字节缓冲区上操作。下图说明了从应用程序,再通过SSLEngine
,然后到使用的传输机制(如Web),然后返回数据流。
左边所示的应用程序在应用程序缓冲区中提供应用程序(明文)数据,并将其传递给SSLEngine
。SSLEngine
对象处理缓冲区中包含的数据或任何握手数据,以生成TLS/DTLS
编码的数据,并将其放置到应用程序提供的网络缓冲区中。然后,应用程序负责使用适当的传输(如右图所示)将网络缓冲区的内容发送给对等端。在接收到来自对等端的TLS/DTLS
编码数据时,应用程序将数据放入网络缓冲区并将其传递给SSLEngine
。SSLEngine
对象处理网络缓冲区的内容以生成握手数据或应用程序数据。
SSLEngine
类的实例可以处于以下状态之一:
-
Creation 创建
已经创建并初始化了
SSLEngine
,但尚未使用。在此阶段,应用程序可以设置任何特定于SSLEngine
的设置(启用的密码套件,SSLEngine
是否应该在客户端模式或服务器模式下握手,等等)。一旦握手开始,任何新的设置(除了客户端/服务器模式)都将用于下一次握手。 -
Initial handshaking 初始握手
初始握手是一个过程,通过这个过程,两个对等方交换通信参数,直到建立
SSLSession
为止。在此阶段不能发送应用程序数据。 -
Application data 应用数据
在建立通信参数并完成握手之后,应用程序数据就可以通过
SSLEngine
流动了。出站应用程序消息被加密并受到完整性保护,而入站消息则反向执行此过程。 -
Rehandshaking 重握手
任何一方都可以在应用数据(Application data)阶段的任何时候请求对会话进行重新协商。新的握手数据可以在应用程序数据之间混合。在开始重新握手阶段之前,应用程序可以重置
TLS/DTLS
通信参数,如启用的密码套件列表和是否使用客户端身份验证,但不能在客户端/服务器模式之间更改。与以前一样,在握手开始之后,直到下一次握手时才会使用任何新的SSLEngine
配置。 -
Closure 关闭
当不再需要连接时,应用程序应关闭
SSLEngine
,并在关闭底层传输机制之前向对等端发送/接收任何剩余消息。一旦引擎关闭,它就不可重用。
了解SSLEngine操作状态
SSLEngine
的状态由SSLEngineResult.Status
表示。
public static enum Status {
BUFFER_UNDERFLOW,
BUFFER_OVERFLOW,
OK,
CLOSED;
private Status() {
}
}
为了指示引擎的状态和应用程序应该采取的操作,SSLEngine.wrap()
和SSLEngine.unwrap()
方法(这俩是编码/解码数据用的方法)返回一个SSLEngineResult
实例。此SSLEngineResult
对象包含两部分状态信息:引擎的总体状态和握手状态。可能的总体状态由SSLEngineResult.Status
表示。有以下几种状态
-
OK
没有错误发生
-
CLOSED
操作关闭了
SSLEngine
或操作无法完成,因为操作已经关闭。 -
BUFFER_UNDERFLOW
输入缓冲区的数据不足,这表明应用程序必须从对等端获取更多数据(例如,从网络读取更多数据)。
-
BUFFER_OVERFLOW
输出缓冲区没有足够的空间来保存结果,这表明应用程序必须清除或扩大目标缓冲区。
下面的例子说明了如何处理由SSLEngine.unwrap()
返回的BUFFER_UNDERFLOW和BUFFER_OVERFLOW状态。 它使用SSLSession.getApplicationBufferSize()
和SSLSession.getPacketBufferSize()
来确定字节缓冲区的大小。
//peerNetData指接收的对等端发来的编码数据,peerAppData用于存储解码后的对等端数据
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
switch (res.getStatus()) {
case BUFFER_OVERFLOW:
// 可能需要增大对等端应用程序缓冲区
if (engine.getSession().getApplicationBufferSize() > peerAppData.capacity()) {
//扩大对等端应用程序数据缓冲区
} else {
// 压缩或清除缓冲区
}
break;
case BUFFER_UNDERFLOW:
// 可能需要扩大对等端网络数据包缓冲区
if (engine.getSession().getPacketBufferSize() > peerNetData.capacity()) {
// 扩大对等端网络数据包缓冲区
} else {
// 压缩或清除缓冲区
}
// 获取更多入站网络数据,然后重试该操作
break;
// 处理其他状态: CLOSED, OK
// ...
}
可能的握手状态由SSLEngineResult.HandshakeStatus
表示。它们表示握手是否已经完成,调用者是否必须从对等端获取更多的握手数据,或者向对等端发送更多的握手数据,等等。
public static enum HandshakeStatus {
NOT_HANDSHAKING,
FINISHED,
NEED_TASK,
NEED_WRAP,
NEED_UNWRAP;
private HandshakeStatus() {
}
}
可以使用以下握手状态:
-
FINISHED
SSLEngine
刚刚完成握手。 -
NEED_TASK
在继续握手之前,
SSLEngine
需要一个(或多个)委托任务的结果。 -
NEED_UNWRAP
在继续握手之前,
SSLEngine
需要从远程接收数据。 -
NEED_UNWRAP_AGAIN
在继续握手之前,
SSLEngine
需要拆包(unwrap)。该值表示以前已经从远程端接收了尚未解释的数据,不需要再次接收;数据已被带入JSSE
框架,但还没有被处理。 -
NEED_WRAP
在继续握手之前,
SSLEngine
必须将数据发送到远程端,因此应该调用SSLEngine.wrap()
。 -
NOT_HANDSHAKING
SSLEngine
当前未进行握手。
每个结果具有两个状态可以使SSLEngine
指示应用程序必须采取两项操作:一项响应握手,另一项代表wrap()
和unwrap()
方法的总体状态。 例如,作为单个SSLEngine.unwrap()
调用的结果,引擎可能返回SSLEngineResult.Status.OK
以指示输入数据已成功处理,而SSLEngineResult.HandshakeStatus.NEED_UNWRAP
则指示应用程序应获取更多的TLS / DTLS
对等方编码的数据并将其再次提供给SSLEngine.unwrap()
,以便握手可以继续。 如你所见,上一个示例已大大简化; 他们将需要扩展以正确处理所有这些状态。
下面例子演示了如何通过检查握手状态以及wrap()
和unwrap()
方法的整体状态来处理握手数据。
建议阅读这个博客来先了解捂手过程:https://blog.csdn.net/www646288178/article/details/112218359
//下面的代码示例演示了如何通过检查握手状态和wrap()和unwrap()方法的总体状态来处理握手数据:
void doHandshake(SocketChannel socketChannel, SSLEngine engine, ByteBuffer myNetData, ByteBuffer peerNetData) throws Exception {
// 创建字节缓冲区以用于保存应用程序数据
int appBufferSize = engine.getSession().getApplicationBufferSize();
ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);
// 开始握手
engine.beginHandshake();
SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();
// 处理握手消息
//当握手正在进行
while (hs != SSLEngineResult.HandshakeStatus.FINISHED &&
hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
switch (hs) {
case NEED_UNWRAP:{
// 接收来自对等端的握手数据
if (socketChannel.read(peerNetData) < 0) {
// 无数据可读的逻辑
}
// 处理传入的握手数据
peerNetData.flip();
//将传入的数据解码(解密),并存入peerAppData缓冲区
SSLEngineResult res = engine.unwrap(peerNetData,
peerAppData);
peerNetData.compact();
hs = res.getHandshakeStatus();
// 检查状态
switch (res.getStatus()) {
case OK :
// 处理状态OK的逻辑
break;
// 处理其他状态: BUFFER_UNDERFLOW, BUFFER_OVERFLOW,CLOSED
// ...
}
break;
}
case NEED_WRAP : {
// 清空本地网络数据包缓冲区。
myNetData.clear();
// 生成握手数据,即将我的app我数据编码(加密)并存入缓冲区myNetData
res = engine.wrap(myAppData, myNetData);
hs = res.getHandshakeStatus();
//检查状态
switch (res.getStatus()) {
case OK: {
myNetData.flip();
//发送握手数据给对等端
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
break;
}
// 处理其他状态: BUFFER_OVERFLOW, BUFFER_UNDERFLOW,CLOSED
// ...
}
break;
}
case NEED_TASK : {
// 处理阻塞任务
break;
}
// 处理其他状态: // FINISHED or NOT_HANDSHAKING
// ...
}
}
// 握手之后的程序
// ...
}
需要注意的是,在握手期间
HandshakeStatus
和Status
的值应该结合起来才具有实际意义。如:HandshakeStatus.wrap + Status.BUFFER_OVERFLOW : 需要编码数据 + 数据缓冲区溢出。这就代表在wrap数据的时候,缓冲区满了,即自己端的网络缓冲区对象myNetBuf满了,需要扩容
HandshakeStatus.unwrap + Status.BUFFER_OVERFLOW : 需要解码数据 + 数据缓冲区溢出。这就代表在unwrap数据的时候,缓冲区满了,即对等端的app数据缓冲区对象peerAppBuf满了,需要扩容。
HandshakeStatus.unwrap + Status.BUFFER_UNDERFLOW : 需要解码数据 + 数据缓冲区数据不足。这就代表在unwrap数据的时候,缓冲区中的数据不够,数据读取不完整,可能是半包问题,或者是peerNetBuf的空间不足。
HandshakeStatus.wrap + Status.BUFFER_UNDERFLOW 的情况不合理,不需要做操作,只记录日志。
SSLEngine用于TLS协议
本节向您展示如何创建一个SSLEngine
对象,并使用它来生成和处理TLS
数据。
创建一个SSLEngine对象
使用SSLContext.createSSLEngine()
方法创建一个SSLEngine
对象。
在创建SSLEngine
对象之前,必须将引擎配置为客户端或服务器模式,并设置其他配置参数,例如要使用哪种密码套件以及是否需要客户端身份验证。SSLContext.createSSLEngine()
方法创建一个javax.net.ssl.SSLEngine
对象。
//使用JKS作为密钥库为TLS创建SSLEngine客户端的示例代码
String hostname = "localhost";
int port = 8888;
char[] passphrase = "passphrase".toCharArray();
//使用密钥材料创建并初始化SSLContext
// 首先初始化密钥和可信材料
KeyStore ksKeys = KeyStore.getInstance("JKS");
ksKeys.load(new FileInputStream("testKeys"), passphrase);
KeyStore ksTrust = KeyStore.getInstance("JKS");
ksTrust.load(new FileInputStream("testTrust"), passphrase);
// 密钥管理器决定使用哪个密钥材料
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ksKeys, passphrase);
// TrustManager决定是否允许连接
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ksTrust);
// 获取TLS协议的SSLContext实例
SSLContext sslContext = SSLContext.getInstance("TLS");
//使用密钥材料和可信材料初始化SSLContext
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// 创建引擎
SSLEngine engine = sslContext.createSSLEngine(hostname, port);
// 设置为客户端模式
engine.setUseClientMode(true);
生成并处理TLS数据
两个主要的SSLEngine
方法是wrap()
和unwrap()
。它们分别负责编码(加密)生成网络数据和解码(解密)网络数据。该数据可能是握手数据或应用程序数据,这取决于SSLEngine
对象的状态,。
每个SSLEngine
对象在其生命周期内有几个阶段。在发送或接收应用程序数据之前,TLS
协议需要一次握手来建立加密参数。此握手需要SSLEngine
对象执行一系列来回步骤。
在初始握手期间,wrap()
和unwrap()
方法生成和使用握手数据,应用程序负责传输这些数据。重复使用wrap()
和unwrap()
方法,直到完成握手。每个SSLEngine
操作都会生成SSLEngineResult
类的一个实例,其中SSLEngineResult.HandshakeStatus
字段用于确定下一步移动握手时必须进行的操作。
下图显示了在一个典型的TLS
握手期间的状态机,以及相应的消息和状态:
握手完成后,对wrap()
的进一步调用将尝试使用应用程序数据并将其打包以进行传输。unwrap()
方法将尝试相反的方法。
要向对等端发送数据,应用程序首先提供它想要通过SSLEngine.wrap()
发送的数据,以获取相应的TLS
编码(加密)数据。然后,应用程序使用其选择的传输机制将编码后的数据发送给对等端。当应用程序通过传输机制从对等端接收到TLS
编码的数据时,它通过SSLEngine.unwrap()
将此数据提供给SSLEngine
,以获取对等端发送的明文数据。
下面的示例是一个使用非阻塞SocketChannel
与对等端通信的SSL
应用程序。通过使用上一个示例中创建的SSLEngine
对字符串进行编码,它将字符串“hello”发送给对等体。它使用来自SSLSession
(后面讲)的信息来确定字节缓冲区的大小
//使用SSLEngine发送和接收数据
String hostname = "localhost";
int port = 8888;
SSLEngine engine = null;//假如我们已获得一个SSLEngine对象
// 创建一个非阻塞的SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(hostname, port));
// 测试是否连接完成
while (!socketChannel.finishConnect()) {
// 在连接完成之前做其他事情
}
//创建用于保存应用程序和编码数据的字节缓冲区
SSLSession session = engine.getSession();
ByteBuffer myAppData =ByteBuffer.allocate(session.getApplicationBufferSize());//我的应用数据的缓冲区
ByteBuffer myNetData =ByteBuffer.allocate(session.getPacketBufferSize());//我的网络数据的缓冲区,即被TLS编码过的、待发送的数据
ByteBuffer peerAppData =ByteBuffer.allocate(session.getApplicationBufferSize());//对等端的应用数据的缓冲区,即解码过的对等端应用数据
ByteBuffer peerNetData =ByteBuffer.allocate(session.getPacketBufferSize());//对等端的网络数据的缓冲区,即被TLS编码过的、待接收的数据
// 做最初的握手,这个方法是前面给的使用wrap()和unwrap()方法检查握手状态的示例
doHandshake(socketChannel, engine, myNetData, peerNetData);
//发送数据给对等端
myAppData.put("hello".getBytes());
myAppData.flip();
while (myAppData.hasRemaining()) {
// 生成TLS / DTLS编码的数据(握手或应用程序数据)
SSLEngineResult res = engine.wrap(myAppData, myNetData);
// 检查状态
if (res.getStatus() == SSLEngineResult.Status.OK) {
myAppData.compact();
// 发送TLS/DTLS编码的数据到对等端
while (myNetData.hasRemaining()) {
int num = socketChannel.write(myNetData);
if (num == 0) {
// 没有写的字节数;稍后再试
}
}
}
// 处理其他状态:BUFFER_OVERFLOW,关闭
//...
}
//接收对等端的TLS/DTLS数据
int num = socketChannel.read(peerNetData);
if (num == -1) {
// 已读到数据流末尾
} else if (num == 0) {
// 没有数据可读,重试
} else {
// 处理读进来的数据
peerNetData.flip();
//解码数据,并放入peerAppData
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
//判断状态
if (res.getStatus() == SSLEngineResult.Status.OK) {
peerNetData.compact();
if (peerAppData.hasRemaining()) {
// 使用对等端数据
}
}
// 处理其他状态:BUFFER_OVERFLOW, BUFFER_UNDERFLOW, CLOSED
//...
}
由这个例子可以很明白地看出,SSLEngine
其实就是一个加密、解密机。它与对等端协商好共享的加解密参数,然后用于将应用数据加密或者将网络数据解密。它本身不处理网络数据流的传输操作。我们可以使用任意类型的网络传输模型来传输网络数据。这样就实现了可定制的、高度灵活的网络数据传输机制。
SSLEngine 用于DTLS协议
本节向您展示如何创建一个SSLEngine
对象,并使用它来处理DTLS
握手、生成和处理DTLS
数据,以及处理DTLS
连接中的重传输问题。
创建一个用于DTLS协议的SSLEngine对象
以下示例说明如何为DTLS
创建SSLEngine
对象。
示例中服务器名和端口号不用于与服务器通信(所有传输都由应用程序负责)。它们是给JSSE提供程序的提示,供DTLS会话缓存使用,也供基于kerberos的密码套件实现使用,以确定应该获得哪些服务器凭据。
//以下示例代码为使用PKCS12作为密钥库的DTLS创建一个SSLEngine客户端:
String hostname = "localhost";
int port = 8888;
// 使用key material创建和初始化SSLContext
char[] passphrase = "passphrase".toCharArray();
// 首先初始化密钥和信任材料
KeyStore ksKeys = KeyStore.getInstance("PKCS12");
ksKeys.load(new FileInputStream("testKeys"), passphrase);
KeyStore ksTrust = KeyStore.getInstance("PKCS12");
ksTrust.load(new FileInputStream("testTrust"), passphrase);
// 密钥管理器决定使用哪种密钥材料
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ksKeys, passphrase);
// TrustManager决定是否允许连接
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ksTrust);
// 获取DTLS协议的SSLContext实例
SSLContext sslContext = SSLContext.getInstance("DTLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
//创建引擎
SSLEngine engine = sslContext.createSSLEngine(hostname, port);
// 作为一个客户端
engine.setUseClientMode(true);
//使用PKCS12作为密钥库为DTLS创建SSLEngine服务器的示例代码
String hostname = "localhost";
int port = 8888;
// 使用密钥材料创建和初始化SSLContext
char[] passphrase = "passphrase".toCharArray();
// 首先初始化密钥和信任材料
KeyStore ksKeys = KeyStore.getInstance("PKCS12");
ksKeys.load(new FileInputStream("testKeys"), passphrase);
KeyStore ksTrust = KeyStore.getInstance("PKCS12");
ksTrust.load(new FileInputStream("testTrust"), passphrase);
// 密钥管理器决定使用哪种密钥材料
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ksKeys, passphrase);
// TrustManager决定是否允许连接
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ksTrust);
// 在没有身份验证的情况下获取DTLS协议的SSLContext
SSLContext sslContext = SSLContext.getInstance("DTLS");
sslContext.init(null, null, null);
// 创建引擎
SSLEngine engine = sslContext.createSSLEngine(hostname, port);
// 作为服务器端
engine.setUseClientMode(false);
// 要求客户端身份认证
engine.setNeedClientAuth(true);
生成和处理DTLS数据
DTLS
握手和TLS
握手以类似的方式生成和处理数据。它们都分别使用SSLEngine.wrap()
和SSLEngine.wrap()
方法来生成和使用网络数据。
下图显示了在典型的DTLS
握手过程中的状态机,以及相应的消息和状态:
- 创建并初始化
DTLS SSLEngine
的实例。 - 如果“握手状态”为
HandshakeStatus.NEED_UNWRAP
,等待网络数据。 - 如果定时器超时,则表明之前传递的握手消息可能已经丢失。
- 调用
SSLEngine.wrap()
。 - 使用Socket发送打包后的数据
在应用程序中处理缓冲的握手消息
数据报传输(DTLS)不要求或提供可靠的或有序的数据传递。握手消息可能会丢失或需要重新排序。在DTLS
实现中,可能需要对握手消息进行缓冲,以便在接收到之前的所有消息之前进行后续处理。
SSLEngine
的DTLS
实现负责对握手消息重新排序。握手消息缓冲和重新排序对应用程序是透明的。
然而,应用程序必须管理HandshakeStatus.NEED_UNWRAP_AGAIN
状态。此状态表明,对于下一个SSLEngine.unwrap()
操作,不需要来自远程端的额外数据。
使用HandshakeStatus.NEED_UNWRAP_AGAIN
的典型场景如下图所示。
- 创建并初始化
DTLS SSLEngine
的实例。 - 可选:当握手状态为
HandshakeStatus.NEED_UNWRAP
时,等待网络数据 - 可选:如果接收到网络数据,则调用
SSLEngine.unwrap()
。 - 确定下一步处理的握手状态。握手状态可以是
HandshakeStatus.NEED_UNWRAP_AGAIN
、HandshakeStatus.NEED_UNWRAP
或HandshakeStatus.NEED_WRAP
。- 如果“握手状态为
HandshakeStatus.NEED_UNWRAP_AGAIN
,调用SSLEngine.unwrap()
。且SSLEngine.unwrap()
操作不需要来自网络的其他数据。
- 如果“握手状态为
- 确定用于进一步处理的握手状态。握手状态可以是
HandshakeStatus.NEED_UNWRAP_AGAIN
、HandshakeStatus.NEED_UNWRAP
或HandshakeStatus.NEED_WRAP
。
处理阻塞任务
在握手期间,SSLEngine
可能会遇到阻塞或花费很长时间的任务。例如,TrustManager
可能需要连接到远程证书验证服务,或者KeyManager
可能需要提示用户确定使用哪个证书作为客户端身份验证的一部分。为了保持SSLEngine
的非阻塞性质,当引擎遇到这样的任务时,它将返回SSLEngineResult.HandshakeStatus.NEED_TASK
。在接收到此状态后,应用程序应该调用SSLEngine.getDelegatedTask()
来获取任务,然后使用适合其需求的线程模型处理任务。例如,应用程序可能从线程池获取线程来处理任务,而主线程处理其他I/O。
下面的代码在一个新创建的线程中执行每个任务:
if (res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
new Thread(task).start();
}
}
此期间SSLEngine
将阻止未来的wrap()
和unwrap()
调用,直到所有未完成的任务都完成。
关闭TLS/DTLS连接
为了有序地关闭TLS/DTLS
连接,TLS/DTLS
协议需要传输关闭消息。因此,当应用程序使用TLS/ DTLS
连接完成时,它应该首先从SSLEngine
获得关闭消息,然后使用其传输机制将它们传输到对等端,最后关闭传输机制(如关闭网络流)。
如下例所示
//下面的代码示例演示了如何关闭TLS/DTLS连接
//设置为关闭状态,表明不再在此SSLEngine上发送出站应用程序数据。
engine.closeOutbound();
//判断数据是否发送完
while (!engine.isOutboundDone()) {
// 获取关闭信息
SSLEngineResult res = engine.wrap(empty, myNetData);
// 检查状态
// 发送关闭信息给对等端
while(myNetData.hasRemaining()) {
int num = socketChannel.write(myNetData);
if (num == 0) {
// 没有数据;稍后再试
}
myNetData.compact();
}
}
// Close transport
socketChannel.close();
4.2.5 SSLSession类和ExtendSSLSession类
javax.net.ssl.SSLSession
接口表示SSLSocket
或SSLEngine
连接的两个对等方之间协商的安全上下文。会话建立后,可以由将来连接在相同两个对等方之间的SSLSocket
或SSLEngine
对象共享该会话。
在某些情况下,在握手后期需要协商握手时的参数,以做出有关信任的决策。例如,有效签名算法的列表可能会限制可用于身份验证的证书类型。可以在握手期间通过在SSLSocket
或SSLEngine
上调用getHandshakeSession()
来获取SSLSession
对象。 TrustManager
或KeyManager
的实现可以使用getHandshakeSession()
方法获取有关会话参数的信息,以帮助他们做出决策。
完全初始化的SSLSession
包含将用于通过安全套接字进行通信的密码套件,以及有关远程对等方的网络地址的非权威性提示,以及诸如创建时间和最后使用时间之类的管理信息。会话还包含对等方之间协商的共享主密钥,该共享主密钥用于SSLSocket
或SSLEngine
连接间的数据加密和完整性保护。
ExtendedSSLSession
扩展了SSLSession
接口,以支持其他会话属性。ExtendedSSLSession
类添加了一些方法,这些方法描述了本地实现的和对等方所支持的签名算法。在ExtendedSSLSession
实例上调用getRequestedServerNames()
方法用于获取 Server Name Indication (SNI) Extension 规定的SNIServerName
对象的列表。
调用SSLSession
上的getPacketBufferSize()
和getApplicationBufferSize()
方法可确定SSLEngine
使用的适当缓冲区大小。
4.2.6 HttpsURLConnection 类
涉及的类:
javax.net.ssl.HttpsURLConnection
javax.net.ssl.HttpsURLConnection
类继承自java.net.HttpURLConnection
类,并添加了一些HTTPS
相关的特性。
HTTPS
协议类似于HTTP
,但是HTTPS
首先通过TLS
套接字建立安全通道,然后在请求或接收数据之前验证对等方的身份。
获得HttpsURLConnection
实例后,可以在实际通过URLConnection.connect()
方法启动网络连接之前,配置多个HTTP
和HTTPS
参数。
设置分配的SSLSocketFactory
在某些情况下,希望指定HttpsURLConnection
实例使用的SSLSocketFactory
。 例如,您可能希望通过默认实现不支持的代理类型进行channel
传输。 新的SSLSocketFactory
可以返回已经执行了所有必要channel
的套接字,从而允许HttpsURLConnection
使用其他代理。
HttpsURLConnection
类具有一个默认的SSLSocketFactory
,该类在加载时分配(这是SSLSocketFactory.getDefault()
方法返回的工厂)。 将来的HttpsURLConnection
实例将聚合(作为内部属性)当前的默认SSLSocketFactory
,直到通过静态HttpsURLConnection.setDefaultSSLSocketFactory()
方法将新的默认SSLSocketFactory
分配给该类为止。 创建HttpsURLConnection
的实例后,可以通过调用setSSLSocketFactory()
方法来覆盖此实例上聚合的SSLSocketFactory
。
注意:更改默认静态
SSLSocketFactory
不会对HttpsURLConnection
的现有实例产生影响。 若要更改现有实例,必须调用setSSLSocketFactory()
方法。
获取分配的HostnameVerifier
如果URL
的主机名与TLS
握手过程中收到的凭据中的主机名不匹配,则可能发生了URL
欺骗。 如果实现无法确定具有合理确定性的主机名,则TLS
实现将对实例的已分配HostnameVerifier
执行回调,以进行进一步检查。 主机名验证程序可以采取任何必要的步骤来进行确定,例如执行主机名模式匹配或打开交互式对话框。 主机名验证程序验证失败,将关闭连接。
setHostnameVerifier()
和setDefaultHostnameVerifier()
方法的操作与setSSLSocketFactory()
和setDefaultSSLSocketFactory()
方法的操作类似,因为HostnameVerifier
对象是按实例和类分配的,并且可以通过以下方式获取当前值: 调用getHostnameVerifier()
或getDefaultHostnameVerifier()
方法。
4.2.7 提供的类和接口
本节中提供的类和接口用以支持SSLContext
对象的创建和初始化,这些对象用于创建SSLSocketFactory
,SSLServerSocketFactory
和SSLEngine
对象。 支持类和接口是javax.net.ssl
包的一部分。
本节中的三个类,SSLContex
,KeyManagerFactory
, 和TrustManagerFactory
,是引擎类(见第一章对引擎类的解释)。JSSE附带的标准SunJSSE提供程序提供SSLContext
,KeyManagerFactory
和TrustManagerFactory
实现,以及标准java.security
API中引擎类的实现。
下表列出SunJSSE提供的算法:
实现的引擎类 | 算法或协议 |
---|---|
KeyStore | PKCS12 |
KeyManagerFactory | PKIX, SunX509 |
TrustManagerFactory | PKIX (X509 or SunPKIX), SunX509 |
SSLContext | SSLv31 , TLSv1, TLSv1.1, TLSv1.2, TLSv1.3, DTLSv1.0, DTLSv1.2 |
SSLContext 类
javax.net.ssl.SSLContext
类是用于实现安全套接字协议的引擎类。 此类的实例充当SSLSocket
,SSLServerSocket
和SSLEngine
的工厂。 SSLContext
对象保存 在该上下文下创建的所有对象之间共享的所有状态信息。 例如,Session状态是由上下文提供的套接字工厂通过握手协议协商时与SSLContext
关联的。 这些缓存的Session可以被在相同上下文下创建的其他套接字重用和共享。
每个实例都通过其init
方法配置执行身份验证所需的密钥、证书链和受信任的根CA证书。 以密钥和信任管理器的形式提供此配置。 这些管理器为上下文支持的密码套件的身份验证和密钥协议方面提供支持。
当前,仅支持基于X.509
的管理器。
获取并初始化一个SSLContext 对象
有两种方式获得并初始化SSLContex
对象:
- 最简单的方式是调用
SSLContext.getDefault
静态方法。这个方法创建一个默认的SSLContext
对象,该对象使用默认的KeyManager
、TrustManager
和SecureRandom
对象。一个默认的KeyManagerFactory
和TrustManagerFactory
被用于创建KeyManager
、TrustManager
和SecureRandom
对象。它使用默认的keystore
和truststore
中的密钥材料。默认的密钥材料配置见第5.3节:定制JSSE,或见官方文档8-52。 - 使调用者对创建的上下文的行为具有最大控制权的方法是调用
SSLContext
类上的静态方法SSLContext.getInstance(String protocal,String provider)
(注意:官方文档上写的还是getDefault
方法,这里应该是getInstance
),然后通过调用实例的某一init()
方法来初始化上下文。init()
方法的一个变体带有三个参数:KeyManager
对象数组,TrustManager
对象数组和SecureRandom
对象。 通过实现适当的接口或使用KeyManagerFactory
和TrustManagerFactory
类来创建KeyManager
和TrustManager
对象。 然后,可以分别使用KeyStore
中包含的密钥材料初始化KeyManagerFactory
和TrustManagerFactory
,这些密钥材料作为参数传递给TrustManagerFactory
或KeyManagerFactory
类的init()
方法。 最后,可以调用getTrustManagers()
方法(在TrustManagerFactory
中和getKeyManagers()
方法(在KeyManagerFactory
中)来获取信任管理器或密钥管理器的数组.
建立TLS
连接后,将创建一个SSLSession
,其中包含各种信息,例如已建立的身份和所使用的密码套件。 然后,SSLSession
用于描述两个实体之间的正在进行的关系和状态信息。 每个TLS
连接一次涉及一个会话,但是该会话可以同时或顺序用于这些实体之间的许多连接。
创建一个SSLContext对象
与其他基于JCA提供程序的引擎类一样,SSLContext
对象是使用SSLContext
类的getInstance(String protocol)
工厂方法创建的。 这些静态方法每个都返回一个实例,该实例至少实现所请求的安全套接字协议。从此上下文对象创建的SSLSocket
,SSLServerSocket
或SSLEngine
对象调用getSupportedProtocols()
方法将返回受支持协议的列表。你可以调用这三个对象的setEnabledProtocols(String [] protocols)
方法来控制为SSL连接实际启用了哪些协议。
注意:当你调用
SSLSocketFactory.getDefault()
方法时,它实际上已经被初始化完成了,所以你不必在使用前对它返回的对象进行初始化,除非你想修改他的配置
调用如下方法获取对象:
public static SSLContext getInstance(String protocol);
public static SSLContext getInstance(String protocol, String provider);//指定服务提供者
public static SSLContext getInstance(String protocol, Provider provider);//制定服务提供者
参数protocol
用于指定需要的安全套接字协议的名字,如TLS
。SSLContext
常用的协议名称见Java Security
Standard Algorithm Names。
例如:
//获取SSLContex实例
SSLContext sc = SSLContext.getInstance("TLS");
可以使用init
方法初始化
public void init(KeyManager[] km, TrustManager[] tm, SecureRandom random);
如果KeyManager []
参数为null
,则将为此上下文定义一个空的KeyManager
。 如果TrustManager []
参数为null
,则将在已安装的安全提供程序中搜索TrustManagerFactory
类的最高优先级实现,从中将获取适当的TrustManager
。 同样,SecureRandom
参数可以为null
,在这种情况下,将使用默认实现。
TrustManager接口
TrustManager
的主要职责是确定所提供的身份验证凭据是否值得信任。如果凭据不受信任,则连接将终止。 要验证安全套接字对等方的远程身份,必须使用一个或多个TrustManager
对象初始化SSLContext
对象。您必须为支持的每种身份验证机制传递一个TrustManager
。 如果将null
传递给SSLContext
来初始化,则将自动为您创建一个信任管理器。通常,单个信任管理器支持基于X.509
公钥证书(例如X509TrustManager
)的身份验证。 一些安全套接字实现也可能支持基于共享密钥,Kerberos或其他机制的身份验证。
TrustManager
对象可以由TrustManagerFactory
创建,也可以通过提供接口的具体实现来创建。
TrustManagerFactory类
javax.net.ssl.TrustManagerFactory
是一个引擎类,该类充当一种或多种类型的TrustManager
对象的工厂。 由于它是基于提供者程序的,因此可以实现并配置其他工厂,以提供其他或替代的信任管理器,这些管理器提供更复杂的服务或实现特定于安装的身份验证策略。
创建TrustManagerFactory类对象
通过以下方式创建:
TrustManagerFactory tmf = TrustManagerFactory.getInstance(String algorithm);
//指定提供者
TrustManagerFactory tmf = TrustManagerFactory.getInstance(String algorithm, String provider);
如:
//创建了SunJSSE提供程序的PKIX信任管理器工厂的实例。 该工厂可用于创建提供基于X.509 PKIX的认证路径有效性检查的信任管理器。
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE");
新创建的对象需要被初始化:
public void init(KeyStore ks);
public void init(ManagerFactoryParameters spec);
对于许多工厂,例如SunJSSE
提供程序提供的SunX509 ``TrustManagerFactory
,使用KeyStore
是初始化TrustManagerFactory
的唯一方式。TrustManagerFactory
将在密钥库中查询有关在授权检查期间应信任哪些远程证书的信息。
某些信任管理器可以不需要而无需使用
KeyStore
对象或任何其他参数进行显式初始化来做出信任决策。 例如,他们可以通过LDAP
从本地目录服务访问信任材料,使用远程在线证书状态检查服务器,或者从标准本地位置访问默认信任材料。
检查TrustManagerFactory对象的证书的到期日期
以下方法filterTrustAnchors
可以用来过滤一组信任锚,删除那些证书将在指定日期之前过期的信任锚,然后使用该组信任锚初始化TrustManagerFactory
对象。
public static void filterTrustAnchors( String truststore, String password, String validityDate) throws Exception {
//读取密钥库
FileInputStream is = new FileInputStream(truststore);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(is, password.toCharArray());
//使用密钥库初始化PKIXParameters
PKIXParameters params = new PKIXParameters(keystore);
// 获取一个CA根证书集合
Set<TrustAnchor> myTrustAnchors = params.getTrustAnchors();
// 创建在指定日期仍然有效的一组新的CA证书
Set<TrustAnchor> validTrustAnchors =
myTrustAnchors.stream().filter(ta -> {
try {
ta.getTrustedCert().checkValidity(
//逐个验证有效期
DateFormat.getDateInstance().parse(validityDate));
} catch (CertificateException | ParseException e) {
return false;
}
return true;
}).collect(Collectors.toSet());
// 使用验证过日期的validTrustAnchors创建 PKIXBuilderParameters 对象
PKIXBuilderParameters pkixParams =new PKIXBuilderParameters(validTrustAnchors, new X509CertSelector());
// 包装 PKIX 参数 为 ManagerFactoryParameters
ManagerFactoryParameters trustParams = new CertPathTrustManagerParameters(pkixParams);
// 为兼容PKIX的信任管理器创建TrustManagerFactory
TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
// 将参数传递给工厂以传递给CertPath实现
factory.init(trustParams);
// 使用工厂
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, factory.getTrustManagers(), null);
}
这种方式比前面的创建SSLContext时的方式区别在于,在使用KeyStore初始化工厂前,先验证KeyStore中根证书的有效性。
PKIX TrustManager 支持
默认的信任管理器算法是PKIX
。 可以通过编辑java.security
文件中的ssl.TrustManagerFactory.algorithm
属性来更改它。
PKIX
信任管理器工厂使用已安装的安全提供程序中的CertPath PKIX
实现。可以使用常规的init(KeyStores)
方法或使用CertPathTrustManagerParameters
类将CertPath
参数传递给PKIX
信任管理器来初始化信任管理器工厂。
下例说明了如何使信任管理器使用特定的LDAP
证书存储并启用吊销检查。
// keystore的密钥
char[] pass = System.console().readPassword("Password: ");
// 创建 PKIX 参数
KeyStore anchors = KeyStore.getInstance("JKS");
//anchorsFile:锚证书的证书文件
anchors.load(new FileInputStream(anchorsFile), pass);
PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(anchors, new X509CertSelector());
// 指定要使用的LDAP证书存储库
LDAPCertStoreParameters lcsp = new LDAPCertStoreParameters("ldap.imc.org", 389);
pkixParams.addCertStore(CertStore.getInstance("LDAP", lcsp));
// 指定要启用吊销检查
pkixParams.setRevocationEnabled(true);
// 将PKIX参数包装为信任管理器参数
ManagerFactoryParameters trustParams = new CertPathTrustManagerParameters(pkixParams);
// 为兼容PKIX的信任管理器创建TrustManagerFactory
TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
// 将参数传递给工厂初始化
factory.init(trustParams);
// 使用工厂
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, factory.getTrustManagers(), null);
X509TrustManager接口
javax.net.ssl.X509TrustManager
接口扩展了常规TrustManager
接口。 使用基于X.509
的身份验证时,必须由信任管理器实现。
为了支持通过JSSE
对远程套接字对等体的X.509
身份验证,必须将此接口的实例传递到SSLContext
对象的init
方法。
创建X509TrustManager类型实例
您可以自己直接实现此接口,也可以从基于提供程序的TrustManagerFactory
(例如,由SunJSSE
提供程序提供的接口)中获取一个。 您还可以实现自己的接口,该接口委托给一个工厂生成的信任管理器。
如果将空的KeyStore
参数传递给``SunJSSE PKIX或
SunX509 TrustManagerFactory`,则工厂将使用以下过程尝试查找信任材料:
- 如果定义了
javax.net.ssl.trustStore
属性,则TrustManagerFactory
尝试使用该系统属性指定的文件名查找文件,并将该文件用作KeyStore
参数。 如果还定义了javax.net.ssl.trustStorePassword
系统属性,则在打开信任库之前,将使用其值检查信任库中数据的完整性。 - 如果未指定
javax.net.ssl.trustStore
系统属性,则:- 如果文件
java-home/lib/security/jssecacerts
存在,则使用它 - 如果文件
java-home/lib/security/cacerts
存在,则使用它 - 如果这两个文件都不存在,则
TLS
密码套件是匿名的,不执行任何身份验证,因此不需要信任库。
- 如果文件
- 在检查
cacerts
文件之前,工厂将查找通过javax.net.ssl.trustStore
安全性属性指定的文件或jssecacerts
文件。 因此,您可以提供一组特定于JSSE
的受信任的根证书,这些证书与cacerts
中可能存在的受信任的根证书分开,以进行代码签名。
创建你自己的X509TrustManager
如果提供的X509TrustManager
行为不适合您的情况,则可以通过创建和注册自己的TrustManagerFactory
或直接实现X509TrustManager
接口来创建自己的X509TrustManager
。
见 官方文档
动态更新KeyStore
见 官方文档
X509ExtendedTrustManager类型的对象
X509ExtendedTrustManager
类是X509TrustManager
接口的抽象实现。 它增加了对连接敏感的信任管理的方法。 此外,它还可以在TLS
层进行端点验证。
在TLS 1.2
和更高版本中,客户端和服务器都可以指定它们将接受的哈希和签名算法。 要对远程端进行身份验证,身份验证决策必须基于X509
证书以及本地接受的哈希和签名算法。 可以使用ExtendedSSLSession.getLocalSupportedSignatureAlgorithms()
方法获得本地接受的哈希和签名算法。
您可以自己创建一个X509ExtendedTrustManager
子类(在下一节中概述),也可以从基于提供程序的TrustManagerFactory
(如SunJSSE
提供程序提供的)中获取一个。
创建你自己的X509ExtendedTrustManager
实现类,见 官方文档
KeyManager接口
KeyManager
的主要职责是选择最终将发送到远程主机的身份验证凭据。 要将您自己(本地安全套接字对等体)认证为远程对等体,必须使用一个或多个KeyManager
对象初始化SSLContext
对象。 您必须为将支持的每种不同的身份验证机制传递一个KeyManager
。 如果将null
传递给SSLContext
初始化,则将创建一个空的KeyManager
。 如果使用内部默认上下文(例如,由SSLSocketFactory.getDefault()
或SSLServerSocketFactory.getDefault()
创建的SSLContext
,则会创建一个默认的KeyManager
。 通常,单个密钥管理器支持基于X.509
公共密钥证书的身份验证。 一些安全套接字实现也可能支持基于共享密钥,Kerberos
或其他机制的身份验证。
KeyManager
对象可以通过KeyManagerFactory
或通过提供接口的具体实现来创建。
KeyManagerFactory 类
javax.net.ssl.KeyManagerFactory
类是基于提供程序的服务的引擎类,该类充当一种或多种类型的KeyManager
对象的工厂。 SunJSSE
提供程序实现了一个工厂,该工厂可以返回基本的X.509
密钥管理器。 因为它是基于提供程序的,所以可以实现并配置其他工厂以提供其他或替代的密钥管理器。
创建KeyManagerFactory 对象
使用以下方法实例化一个工厂
KeyManagerFactory kmf = getInstance(String algorithm);
KeyManagerFactory kmf = getInstance(String algorithm, String provider);
KeyManagerFactory kmf = getInstance(String algorithm, Provider provider);
如:
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509", "SunJSSE");
使用以下方法初始化工厂:
//SunJSSE实现的,也是默认的KeyManagerFactory 只能用这个方法实例化。KeyManagerFactory将查询KeyStore,以获取有关应使用哪些私钥和匹配的公钥证书对远程套接字对等方进行身份验证的信息。password参数指定将与用于从KeyStore访问密钥的方法一起使用的密码。 密钥库中的所有密钥都必须使用相同的密码保护。
public void init(KeyStore ks, char[] password);
//有时提供程序需要除KeyStore和密码之外的初始化参数。 该提供者的用户应通过提供者定义的适当ManagerManagerParameters的实现。 然后,提供程序可以调用ManagerFactoryParameters实现中的指定方法来获取所需的信息。
public void init(ManagerFactoryParameters spec);
一些工厂可以提供对身份验证材料的访问,而无需使用KeyStore
对象或任何其他参数进行初始化。 例如,他们可以作为登录机制(例如基于JAAS
,Java身份验证和授权服务的一种)的登录机制的一部分来访问密钥资料。
X509KeyManager接口
javax.net.ssl.X509KeyManager
接口扩展了常规KeyManager
接口。 它必须由密钥管理器实现,以用于基于X.509
的身份验证。 为了支持通过JSSE
对远程套接字对等方的X.509
身份验证,必须将此接口的实例传递到SSLContext
对象的init()
方法。
创建一个X509KeyManager类型的对象
您可以直接自己实现此接口,也可以从基于提供程序的KeyManagerFactory
(例如由SunJSSE
提供程序提供的KeyManagerFactory
)中获得一个。 您还可以实现自己的接口,该接口委托给工厂生成的密钥管理器。 例如,您可以执行此操作以过滤结果键并通过图形用户界面查询最终用户。
可以像创建自己的X509ExtendedTrustManager
类型对象一样创建你自己的X509KeyManager
类型的对象
X509ExtendedKeyManager类
X509ExtendedKeyManager
抽象类是X509KeyManager
接口的实现,该接口允许选择特定于连接的密钥。 它添加了两种根据 密钥类型、允许的颁发者和当前的SSLEngine
为客户端或服务器选择密钥别名的方法:
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine)
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine)
TrustManager 和 KeyManager之间的关系
历史上,关于TrustManager
和KeyManager
的功能一直存在混淆。
-
TrustManager
确定是否应该信任远程身份验证凭据(以及连接)。也就是用于验证远程对等端的证书。 -
KeyManager
确定要发送到远程主机的身份验证凭据。是要发送到远程对等端、用于对方验证自己身份的信息。如自己的公钥证书。
4.2.8 辅助支持类和接口
SSLParameters 类
SSLParameters
类封装了以下影响SSL / TLS / DTLS
连接的参数:
TLS / DTLS
握手中接受的密码套件列表- 允许的协议列表
TLS / DTLS
握手期间的端点识别算法- 服务器名称和服务器名称匹配器(请参阅 SNI)
TLS / DTLS
握手中使用的密码套件首选项TLS / DTLS
握手期间的算法- 服务器名称指示(
SNI
) - 最大网络数据包大小
- 算法约束以及
TLS / DTLS
服务器是否应请求或要求客户端身份验证
您可以使用以下方法检索SSLSocket
或SSLEngine
的当前SSLParameters
:
//SSLSocket, SSLServerSocket, 和 SSLEngin的如下方法
getSSLParameters()
//SSLContext的如下方法
getDefaultSSLParameters()
getSupportedSSLParamters()
您可以在SSLSocket
,SSLServerSocket
和SSLEngine
中使用setSSLParameters
方法指定SSLParameters
。
您可以使用SSLParameters.setServerNames
方法显式设置服务器名称指示。 客户端模式下的服务器名称指示也会影响端点标识。 在X509ExtendedTrustManager
的实现中,它使用ExtendedSSLSession.getRequestedServerNames
方法检索的服务器名称指示。
如下例:
SSLSocketFactory factory = ...
SSLSocket sslSocket = factory.createSocket("172.16.10.6", 443);
// SSLEngine sslEngine = sslContext.createSSLEngine("172.16.10.6", 443);
SNIHostName serverName = new SNIHostName("www.example.com");
List<SNIServerName> serverNames = new ArrayList<>(1);
serverNames.add(serverName);
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(serverNames);
sslSocket.setSSLParameters(params);
// sslEngine.setSSLParameters(params);
密码套件偏好设置
在TLS
握手期间,客户端从其首选项开始,请求从其支持的加密选项列表中协商密码套件。 然后,服务器从客户端请求的密码套件列表中选择一个密码套件。 通常,选择会尊重客户的偏好。 但是,为了减轻使用弱密码套件的风险,服务器可以通过调用SSLParameters.setUseCipherSuitesOrder(true)
方法,根据自己的偏好而不是客户端的偏好来选择密码套件。
SSLSessionContext 接口
javax.net.ssl.SSLSessionContext
接口是与单个实体关联的一组SSLSession
对象。 例如,它可以与同时参与许多会话的服务器或客户端关联。 此接口中的方法启用上下文中所有会话的枚举,并允许通过其会话ID
查找特定会话。
可以选择通过调用SSLSession getSessionContext()
方法从SSLSession
获得SSLSessionContext
。 在某些环境中,该上下文可能不可用,在这种情况下,getSessionContext()
方法返回null
。
SSLSessionBindingListener 接口
javax.net.ssl.SSLSessionBindingListener
接口由在绑定到SSLSession
或从SSLSession
取消绑定时通知的对象实现。也是就是监听SSLSession
的绑定事件。
SSLSessionBindingEvent 类
javax.net.ssl.SSLSessionBindingEvent
类定义了与SSLSession
绑定或解除绑定时传递给SSLSessionBindingListener
的事件。
HandShakeCompletedListener 接口
javax.net.ssl.HandShakeCompletedListener
接口是可以由任何类实现的接口,实现该接口的类可以在给定的SSLSocket
连接上收到SSL
协议握手完成的通知。
HandShakeCompletedEvent 类
javax.net.ssl.HandShakeCompletedEvent
类定义在给定SSLSocket
连接上完成SSL
协议握手后传递给HandShakeCompletedListener
的事件。
HostnameVerifier 接口
如果SSL / TLS
实现的标准主机名验证逻辑失败,则该实现将调用实现此接口并分配给此HttpsURLConnection
实例的类的verify()
方法。 如果回调类可以在给定参数的情况下确定主机名是可接受的,则它将报告应允许连接。 不可接受的响应会导致连接终止。
如下例:
public class MyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
// 弹出一个交互式对话框
//或插入其他匹配逻辑
if (good_address) {
return true;
} else {
return false;
}
}
}
HttpsURLConnection urlc = (HttpsURLConnection)
(new URL("https://www.example.com/")).openConnection();
urlc.setHostnameVerifier(new MyHostnameVerifier());
X509Certificate 类
许多安全套接字协议使用公钥证书(也称为X.509
证书)执行身份验证。 这是TLS
协议的默认身份验证机制。
java.security.cert.X509Certificate
抽象类提供了一种访问X.509
证书属性的标准方法。
AlgorithmConstraints 接口
java.security.AlgorithmConstraints
接口用于控制允许的加密算法。AlgorithmConstraints
定义了三种allows()
方法。 这些方法告诉某些加密功能是否允许使用算法名称或密钥。 加密功能由一组CryptoPrimitive
表示,CryptoPrimitive
是一个枚举,其中包含STREAM_CIPHER,MESSAGE_DIGEST
和SIGNATURE
之类的字段。
因此,AlgorithmConstraints
实现可以回答类似的问题:我可以将此密钥与该算法一起用于加密操作吗?
通过使用新的setAlgorithmConstraints()
方法,可以将AlgorithmConstraints
对象与SSLParameters
对象关联。 使用getAlgorithmConstraints()
方法检索SSLParameters
对象的当前AlgorithmConstraints
对象。
StandardConstants 类
StandardConstants
类用于表示JSSE
中的标准常量定义。StandardConstants.SNI_HOST_NAME
表示服务器名称指示(SNI
)扩展名中的域名服务器(DNS
)主机名,可在实例化SNIServerName
或SNIMatcher
对象时使用。
SNIServerName 类
抽象SNIServerName
类的实例在服务器名称指示(SNI
)扩展名中表示服务器名称。 它使用指定服务器名称的类型和编码值实例化。
您可以使用getType()
和getEncoded()
方法分别返回服务器名称类型和编码的服务器名称值的副本。 equals()
方法可用于检查某个其他对象是否与此服务器名称“相等”。 hashCode
方法返回此服务器名称的哈希码值。 要获取服务器名称的字符串表示形式(包括服务器名称类型和编码的服务器名称值),请使用toString
方法。
SNIMatcher 类
抽象SNIMatcher
类的实例对SNIServerName
对象执行匹配操作。 服务器可以使用来自服务器名称指示(SNI
)扩展名的信息来确定特定的SSLSocket
或SSLEngine
是否应接受连接。 例如,当多个“虚拟”或“基于名称”的服务器托管在单个基础网络地址上时,服务器应用程序可以使用SNI
信息来确定此服务器是否是客户端要访问的确切服务器。 服务器可以使用此类的实例来验证特定类型的可接受服务器名称,例如主机名。
SNIMatcher
类使用指定的服务器名称类型实例化,将在该服务器名称类型上执行匹配操作。 要匹配给定的SNIServerName
,请使用matchs
方法。 要返回给定SNIMatcher
对象的服务器名称类型,请使用getType
方法。
SNIHostName 类
SNIHostName
类的实例(扩展了SNIServerName
类)在服务器名称指示(SNI
)扩展中表示类型为“主机名”的服务器名称。 要实例化SNIHostName
,请指定服务器的标准DNS
主机名(由客户端理解)作为String参数。如下情况时参数非法:
- 该参数为空。
- 参数以尾随句号结尾。
- 该参数不是符合RFC 3490规范的有效国际化域名(IDN)。
- 该参数未使用UTF-8或US-ASCII编码。
4.3 定制JSSE
暂未学习,详见 见 官方文档
4.4 代码示例
SSLEngine
的代码示例可以参考 代码示例
4.4.1 不安全的Socket与安全的Socket创建方式对比
该示例在5.2.0节已经给出过,为方便查看,这里再写一次,这个例子的实现方法不能用于生产实践中,看着感受一下就行。
非安全的套接字程序我们一般这么写
//服务端
int port = 8888;
ServerSocket s=null;
try {
//创建服务器套接字
s = new ServerSocket(port);
//接收连接请求
Socket accept = s.accept();
//获取网络输入输出流
OutputStream out = accept.getOutputStream();
InputStream in = accept.getInputStream();
//.....剩下的步骤
} catch (IOException e) {
e.printStackTrace();
}
//客户端
int port = 8888;
String host = "localhost";
try {
//连接服务器
Socket s = new Socket(host, port);
//获取网络输入输出流
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
//...剩下的步骤
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
简单的安全套接字程序我们这么写
//服务器端
int port = 8888;
SSLServerSocket s=null;
try {
//创建SSL安全服务端套接字工厂
SSLServerSocketFactory sslSrvFact = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
//创建服务器套接字
s = (SSLServerSocket)sslSrvFact.createServerSocket(port);
//接收连接请求
SSLSocket accept = (SSLSocket)s.accept();
//获取网络输入输出流
OutputStream out = accept.getOutputStream();
InputStream in = accept.getInputStream();
//.....剩下的步骤
} catch (IOException e) {
e.printStackTrace();
}
//客户端
int port = 8888;
String host = "localhost";
try {
//创建客户端安全套接字工厂
SSLSocketFactory sslFact = (SSLSocketFactory)SSLSocketFactory.getDefault();
//连接服务器,创建安全套接字
SSLSocket s = (SSLSocket)sslFact.createSocket(host, port);
//获取网络输入输出流
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
//...剩下的步骤
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
4.4.2 运行JSSE示例代码
官方文档,还是建议自己到网上找代码实现的例子,官方给的例子也不全面。
4.4.3 创建用于JSSE的密钥库
本节演示如何使用keytool
实用程序创建适合与JSSE
一起使用的简单PKCS12
密钥库。kettool
工具使用见3.3节。
首先,在密钥库中创建一个keyEntry
(带有公共和私有密钥),然后在信任库中创建一个对应的trustCertEntry
(仅公共密钥)。 对于客户端身份验证,对客户端的证书遵循类似的过程。
-
创建一个新的密钥库和具有相应公钥和私钥的自签名证书。
% keytool -genkeypair -alias duke -keyalg RSA -validity 7 -keystore keystore Enter keystore password: <password> What is your first and last name? [Unknown]: Duke What is the name of your organizational unit? [Unknown]: Java Software What is the name of your organization? [Unknown]: Oracle, Inc. What is the name of your City or Locality? [Unknown]: Palo Alto What is the name of your State or Province? [Unknown]: CA What is the two-letter country code for this unit? [Unknown]: US Is CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US correct? [no]: yes
-
检查密钥库。 请注意,条目类型为
PrivatekeyEntry
,这意味着该条目具有与其关联的私钥。% keytool -list -v -keystore keystore Enter keystore password: <password> Keystore type: PKCS12 Keystore provider: SUN Your keystore contains 1 entry Alias name: duke Creation date: Jul 25, 2016 Entry type: PrivateKeyEntry Certificate chain length: 1 Certificate[1]: Owner: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Issuer: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Serial number: 210cccfc Valid from: Mon Jul 25 10:33:27 IST 2016 until: Mon Aug 01 10:33:27 IST 2016 Certificate fingerprints: SHA1: 80:E5:8A:47:7E:4F:5A:70:83:97:DD:F4:DA:29:3D:15:6B:2A:45:1F SHA256: ED:3C:70:68:4E:86:35:9C:63:CC:B9:59:35:58:94:1F:7E:B8:B0:EE:D2: 4B:9D:80:31:67:8A:D4:B4:7A:B5:12 Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: RSA (2048) Version: 3 Extensions: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 7F C9 95 48 42 8D 68 91 BA 1E E6 5C 2C 6B FF 75 ...HB.h....\,k.u 0010: 5F 19 78 43 _.xC ] ]
-
导出并检查自签名证书。
% keytool -export -alias duke -keystore keystore -rfc -file duke.cer Enter keystore password: <password> Certificate stored in file <duke.cer> % cat duke.cer -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIEIQzM/DANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJV UzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UEChMMT3Jh Y2xlLCBJbmMuMRYwFAYDVQQLEw1KYXZhIFNvZnR3YXJlMQ0wCwYDVQQDEwREdWtl MB4XDTE2MDcyNTA1MDMyN1oXDTE2MDgwMTA1MDMyN1owbDELMAkGA1UEBhMCVVMx CzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDE9yYWNs ZSwgSW5jLjEWMBQGA1UECxMNSmF2YSBTb2Z0d2FyZTENMAsGA1UEAxMERHVrZTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ7+Yeu6HDZgWwkGlG4iKH9w vGKrxXVR57FaFyheMevrgj1ovVnQVFhfdMvjPkjWmpqLg6rfTqU4bKbtoMWV6+Rn uQrCw2w9xNC93hX9PxRa20UKrSRDKnUSvi1wjlaxfj0KUKuMwbbY9S8x/naYGeTL lwbHiiMvkoFkP2kzhVgeqHjIwSz4HRN8vWHCwgIDFWX/ZlS+LbvB4TSZkS0ZcQUV vJWTocOd8RB90W3bkibWkWq166XYGE1Nq1L4WIhrVJwbav6ual69yJsEpVcshVkx E1WKzJg7dGb03to4agbReb6+aoCUwb2vNUudNWasSrxoEFArVFGD/ZkPT0esfqEC AwEAAaMhMB8wHQYDVR0OBBYEFH/JlUhCjWiRuh7mXCxr/3VfGXhDMA0GCSqGSIb3 DQEBCwUAA4IBAQAmcTm2ahsIJLayajsvm8yPzQsHA7kIwWfPPHCoHmNbynG67oHB fleaNvrgm/raTT3TrqQkg0525qI6Cqaoyy8JA2fAp3i+hmyoGHaIlo14bKazaiPS RCCqk0J8vwY3CY9nVal1XlHJMEcYV7X1sxKbuAKFoAJ29E/p6ie0JdHtQe31M7X9 FNLYzt8EpJYUtWo13B9Oufz/Guuex9PQ7aC93rbO32MxtnnCGMxQHlaHLLPygc/x cffGz5Xe5s+NEm78CY7thgN+drI7icBYmv4navsnr2OQaD3AfnJ4WYSQyyUUCPxN zuk+B0fbLn7PCCcQspmqfgzIpgbEM9M1/yav -----END CERTIFICATE-----
或者,您可以使用
-certreq
生成一个证书签名请求(CSR)并将其发送给证书颁发机构(CA)进行签名,但这不在本示例的范围之内。 -
将证书导入到新的信任库中。
% keytool -import -alias dukecert -file duke.cer -keystore truststore Enter keystore password: <password> Re-enter new password: Owner: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Issuer: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Serial number: 210cccfc Valid from: Mon Jul 25 10:33:27 IST 2016 until: Mon Aug 01 10:33:27 IST 2016 Certificate fingerprints: SHA1: 80:E5:8A:47:7E:4F:5A:70:83:97:DD:F4:DA:29:3D:15:6B:2A:45:1F SHA256: ED:3C:70:68:4E:86:35:9C:63:CC:B9:59:35:58:94:1F:7E:B8:B0:EE:D2: 4B:9D:80:31:67:8A:D4:B4:7A:B5:12 Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: RSA (2048) Version: 3 Extensions: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 7F C9 95 48 42 8D 68 91 BA 1E E6 5C 2C 6B FF 75 ...HB.h....\,k.u 0010: 5F 19 78 43 _.xC ] ] Trust this certificate? [no]: yes Certificate was added to keystore
-
检查信任库。 请注意,条目类型为
TrustedCertEntry
,这意味着私钥不适用于该条目。 这也意味着此文件不适合用作KeyManager
的密钥库。% keytool -list -v -keystore truststore Enter keystore password: <password> Keystore type: PKCS12 Keystore provider: SUN Your keystore contains 1 entry Alias name: dukecert Creation date: Jul 25, 2016 Entry type: trustedCertEntry Owner: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Issuer: CN=Duke, OU=Java Software, O="Oracle, Inc.", L=Palo Alto, ST=CA, C=US Serial number: 210cccfc Valid from: Mon Jul 25 10:33:27 IST 2016 until: Mon Aug 01 10:33:27 IST 2016 Certificate fingerprints: SHA1: 80:E5:8A:47:7E:4F:5A:70:83:97:DD:F4:DA:29:3D:15:6B:2A:45:1F SHA256: ED:3C:70:68:4E:86:35:9C:63:CC:B9:59:35:58:94:1F:7E:B8:B0:EE:D2: 4B:9D:80:31:67:8A:D4:B4:7A:B5:12 Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: RSA (2048) Version: 3 Extensions: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 7F C9 95 48 42 8D 68 91 BA 1E E6 5C 2C 6B FF 75 ...HB.h....\,k.u 0010: 5F 19 78 43 _.xC ] ] ******************************************* *******************************************
-
现在,使用适当的密钥库运行您的应用程序。 因为此示例假定使用默认的
X509KeyManager
和X509TrustManager
,所以您可以使用“定制JSSE
”中描述的系统属性来选择密钥库。% java -Djavax.net.ssl.keyStore=keystore -Djavax.net.ssl.keyStorePassword=password Server % java -Djavax.net.ssl.trustStore=truststore -Djavax.net.ssl.trustStorePassword=trustword Client
4.4.4 使用服务器名称指示(SNI)扩展
见 官方文档
五、Java通用安全服务 Java Generic Security Services (Java GSS-API)
六、Java认证和授权服务 Java Authentication and Authorization Service (JAAS)
七、椭圆曲线
椭圆曲线的数学支持在包org.bouncycastle.math.ec中提供。你可以在org.bouncycastle.jce.spec和org.bouncycastle.jce.interfaces两个包中找到与它在加密中使用相关的类和接口。如果使用JDK 1.4或更早版本,则需要使用这两个包。随着JDK 1.5的出现,这两个包进行了轻微的调整,以便JDK 1.5 API和Bouncy Castle可以同时存在,在大多数情况下,您不再需要使用Bouncy Castle包。有一个接口和一个类在JDK 1.5中仍然有用。它们是ECPointEncoder接口和ECNamedCurveSpec类。
椭圆曲线相关接口
代表椭圆曲线密钥的接口在包org.bouncycastle.jce.interfaces中。尽管当你迁移到JDK 1.5时,它们中的大多数都不再相关,但如果你需要与其他无法处理压缩格式的点的提供者合作,ECPointEncoder接口仍然是有用的。
以下Key相关的接口在JCA中均有同名接口,接口中有功能相同但名称不同的方法。
ECKey 接口
ECKey
接口有一个方法getParameters()
,它返回一个ECParameterSpec
,表示密钥关联的椭圆曲线的域参数。
ECPrivateKey 接口
ECPrivateKey
接口有一个方法getD(
,它返回一个表示椭圆曲线私钥私有值的BigInteger
。
ECPublicKey 接口
ECPublicKey
接口有一个方法getQ()
,它返回表示椭圆曲线公钥的公共点的ECPoint
。
ECPointEncoder 接口
所有的Bouncy Castle
椭圆曲线密钥类都实现了这个接口,即使是在JDK 1.5之后。
ECPointEncoder
接口有一个方法setPointFormat()
,它接受一个字符串,表示当实现此接口的EC
密钥被调用其getEncoded()
方法时希望使用的编码风格。
默认情况下,Bouncy Castle
椭圆曲线密钥使用点压缩编码。这可能会导致其他不支持它的提供商出现问题。如果您将UNCOMPRESSED
传递给setPointFormat()
方法,那么将对不使用压缩编码。
椭圆曲线类
spec
包中还提供了一组用于表示密钥和参数规范的类,它们可以与椭圆曲线加密一起使用。
ECNamedCurveParameterSpec类
ECNamedCurveParameterSpec
类可以在任何JDK中使用,尽管在使用JDK 1.5时使用它的继承者ECNamedCurveSpec
可能会更容易。ECNamedCurveParameterSpec
提供了一种机制,用于表示命名曲线的参数规范。可以使用这个类创建密钥,将其参数编码为命名曲线,而不是显式存储构成曲线参数信息的数字。这将导致更小的编码,但这确实意味着使用证书的任何人都必须知道如何从带有公钥的证书中携带的曲线参数名称(通常存储为OID)重新创建曲线参数。
提供程序支持X9.62中的以下命名曲线:
- q prime192v1
- q prime192v2
- q prime192v3
- q prime239v1
- q prime239v2
- q prime239v3
- q prime256v1
它还支持以下与ECGOST3410 (GOST-3410-2001)使用的命名曲线:
- q GostR3410-2001-CryptoPro-A
- q GostR3410-2001-CryptoPro-B
- q GostR3410-2001-CryptoPro-C
- q GostR3410-2001-CryptoPro-XchA
- q GostR3410-2001-CryptoPro-XchB
ECNamedCurveSpec 类
ECNamedCurveSpec
类只存在于JDK 1.5及以上版本中。它提供了一种机制来表示命名曲线的参数规范。与它的前辈ECNamedCurveParameterSpec
一样,这个类创建的密钥将把它们的参数编码为命名曲线,而不是显式地存储曲线信息。
ECParameterSpec 类
ECParameterSpec
类是一个简单的值对象,用于保存椭圆曲线的域参数。
在JDK 1.5中,它已经被java.security.spec
包中同名的类所取代。ECParameterSpec
可以使用曲线、基点(G)、基数(N)、余因子(H)和随机种子(如果可用且有一系列get()方法检索这些值的话)来构造。
ECPrivateKeySpec 类
椭圆曲线私钥的透明表示
ECPrivateKeySpec
类是一个简单的值对象,它包含创建椭圆曲线私钥所需的参数。在JDK 1.5中,它被包java.security.spec
中同名的类替换。
ECPrivateKeySpec
可以用私钥值和ECParameterSpec
构造。类上有用于检索这些值的get()
方法。
ECPublicKeySpec 类
椭圆曲线公钥的透明表示
ECPublicKeySpec
类是一个简单的值对象,它包含创建椭圆曲线公钥所需的参数。在JDK 1.5中,它被包java.security.spec
中同名的类替换。